From 89f80eb2635d9851f9b0ca20920a6cf12239f28f Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:11:20 -0700 Subject: [PATCH 01/30] feat: epoch 3.5 basic setup --- docker-compose.yml | 8 ++++++-- stacks-krypton-miner.toml | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6b262cc..93deacc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: "3.9" x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 1cba695b09690790b1220b0f5d149f70f78ddea5 # 3.3.0.0.1 - - &STACKS_API_COMMIT 7ee1adbddac269d0c03567677013470a62e92f99 # 8.13.4 + - &STACKS_BLOCKCHAIN_COMMIT 3425ccdcdce92c4bd5e1efce5bef136b3e65c473 # feat/epoch-3-5-rc + - &STACKS_API_COMMIT cb5881749553a34704cab62a9631341cb30f2c17 # 8.13.4 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za - &BITCOIN_PEER_PORT 18444 @@ -25,6 +25,8 @@ x-common-vars: - &STACKS_31_HEIGHT ${STACKS_31_HEIGHT:-132} - &STACKS_32_HEIGHT ${STACKS_32_HEIGHT:-133} - &STACKS_33_HEIGHT ${STACKS_33_HEIGHT:-134} + - &STACKS_34_HEIGHT ${STACKS_34_HEIGHT:-135} + - &STACKS_35_HEIGHT ${STACKS_35_HEIGHT:-136} - &STACKING_CYCLES ${STACKING_CYCLES:-1} # number of cycles to stack-stx or stack-extend for - &POX_PREPARE_LENGTH ${POX_PREPARE_LENGTH:-5} - &POX_REWARD_LENGTH ${POX_REWARD_LENGTH:-20} @@ -256,6 +258,8 @@ services: STACKS_31_HEIGHT: *STACKS_31_HEIGHT STACKS_32_HEIGHT: *STACKS_32_HEIGHT STACKS_33_HEIGHT: *STACKS_33_HEIGHT + STACKS_34_HEIGHT: *STACKS_34_HEIGHT + STACKS_35_HEIGHT: *STACKS_35_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH REWARD_RECIPIENT: *REWARD_RECIPIENT diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index 772d200..4cc79ce 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -122,6 +122,14 @@ start_height = $STACKS_32_HEIGHT epoch_name = "3.3" start_height = $STACKS_33_HEIGHT +[[burnchain.epochs]] +epoch_name = "3.4" +start_height = $STACKS_34_HEIGHT + +[[burnchain.epochs]] +epoch_name = "3.5" +start_height = $STACKS_35_HEIGHT + [[ustx_balance]] address = "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6" From be9be9aeece6d55e59a510890828a5f68bb674cc Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:11:48 -0700 Subject: [PATCH 02/30] feat: improve performance by caching stacks-node deps --- Dockerfile.stacks-node | 19 ++++++++++--------- docker-compose.yml | 1 + run.sh | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Dockerfile.stacks-node b/Dockerfile.stacks-node index b6ef52b..397b8e0 100644 --- a/Dockerfile.stacks-node +++ b/Dockerfile.stacks-node @@ -1,27 +1,28 @@ FROM rust:bookworm AS builder -# TODO: is there a built-in required arg syntax? -ARG GIT_COMMIT -RUN test -n "$GIT_COMMIT" || (echo "GIT_COMMIT not set" && false) - -RUN echo "Building stacks-node from commit: https://github.com/stacks-network/stacks-blockchain/commit/$GIT_COMMIT" - RUN apt-get update && apt-get install -y libclang-dev RUN rustup toolchain install stable RUN rustup component add rustfmt --toolchain stable +ARG GIT_COMMIT +RUN test -n "$GIT_COMMIT" || (echo "GIT_COMMIT not set" && false) +RUN echo "Building stacks-node from commit: https://github.com/stacks-network/stacks-blockchain/commit/$GIT_COMMIT" + WORKDIR /stacks RUN git init && \ git remote add origin https://github.com/stacks-network/stacks-blockchain.git && \ git -c protocol.version=2 fetch --depth=1 origin "$GIT_COMMIT" && \ git reset --hard FETCH_HEAD -RUN cargo build --package stacks-node --package stacks-signer --bin stacks-node --bin stacks-signer +RUN --mount=type=cache,target=/stacks/target \ + --mount=type=cache,target=/usr/local/cargo/registry \ + cargo build --package stacks-node --package stacks-signer --bin stacks-node --bin stacks-signer && \ + cp target/debug/stacks-node target/debug/stacks-signer /usr/local/bin/ FROM debian:bookworm -COPY --from=builder /stacks/target/debug/stacks-node /usr/local/bin/ -COPY --from=builder /stacks/target/debug/stacks-signer /usr/local/bin/ +COPY --from=builder /usr/local/bin/stacks-node /usr/local/bin/ +COPY --from=builder /usr/local/bin/stacks-signer /usr/local/bin/ COPY --from=dobtc/bitcoin:25.1 /opt/bitcoin-*/bin /usr/local/bin diff --git a/docker-compose.yml b/docker-compose.yml index 93deacc..3ba7c08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -358,6 +358,7 @@ services: networks: - stacks image: "postgres:15" + pull_policy: if_not_present ports: - "5490:5490" volumes: diff --git a/run.sh b/run.sh index 9f21744..edaa465 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -docker compose down --volumes --remove-orphans --timeout=1 --rmi=all +docker compose down --volumes --remove-orphans --timeout=1 --rmi=local # docker compose up --build docker compose up --build --exit-code-from monitor \ No newline at end of file From 4d045103c1443e2bbdad42f9d1ab16295856adea Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:31:27 -0700 Subject: [PATCH 03/30] feat: start of logic to make bitcoin lockups --- Dockerfile.stacker | 2 +- docker-compose.yml | 58 +++++++++ stacking/btc-locking.ts | 63 ++++++++++ stacking/btc-rpc.ts | 64 ++++++++++ stacking/btc-staker.ts | 260 ++++++++++++++++++++++++++++++++++++++++ stacking/common.ts | 2 + stacking/monitor.ts | 12 +- stacking/package.json | 3 + 8 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 stacking/btc-locking.ts create mode 100644 stacking/btc-rpc.ts create mode 100644 stacking/btc-staker.ts diff --git a/Dockerfile.stacker b/Dockerfile.stacker index 83e84cc..46ef1e3 100644 --- a/Dockerfile.stacker +++ b/Dockerfile.stacker @@ -6,6 +6,6 @@ WORKDIR /root COPY ./stacking/package.json /root/ RUN npm i -COPY ./stacking/stacking.ts ./stacking/common.ts ./stacking/monitor.ts ./stacking/tx-broadcaster.ts /root/ +COPY ./stacking/stacking.ts ./stacking/common.ts ./stacking/monitor.ts ./stacking/tx-broadcaster.ts ./stacking/btc-staker.ts ./stacking/btc-rpc.ts ./stacking/btc-locking.ts /root/ CMD ["npx", "tsx", "/root/stacking.ts"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3ba7c08..fe62942 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -144,9 +144,20 @@ services: i=$$((i + 1)) done + # Create the btc_staking wallet (used by the btc-staker container) + if ! bitcoin-cli -rpcconnect=bitcoind listwallets | grep -q "btc_staking"; then + bitcoin-cli -rpcconnect=bitcoind -named createwallet wallet_name="btc_staking" descriptors=false load_on_startup=true + echo "Created btc_staking wallet" + fi + STAKING_WALLET_ADDRESS=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="btc_staking" getnewaddress "staking_fund") + # Generate the initial blocks to fund the Bitcoin miner address bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" -named generatetoaddress nblocks=$${INIT_BLOCKS} address="$${BITCOIN_MINER_ADDRESS}" + # Fund the staking wallet + bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" -named sendtoaddress address="$${STAKING_WALLET_ADDRESS}" amount=10.0 + echo "Funded btc_staking wallet with 10 BTC" + DEFAULT_TIMEOUT=$$(($$(date +%s) + 30)) while true; do TX_FOUND=false @@ -192,6 +203,15 @@ services: i=$$((i + 1)) done + # Top up btc_staking wallet if low (include unconfirmed balance) + STAKING_BALANCE=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="btc_staking" getbalances | jq '.mine.trusted + .mine.untrusted_pending') + STAKING_BALANCE=$${STAKING_BALANCE:-0} + if (( $$(echo "$${STAKING_BALANCE} < 1.0" | bc -l) )); then + STAKING_ADDR=$$(bitcoin-cli -rpcconnect=bitcoind -rpcwallet="btc_staking" getnewaddress "staking_topup") + bitcoin-cli -rpcconnect=bitcoind -rpcwallet="main" -named sendtoaddress address="$${STAKING_ADDR}" amount=5.0 + echo "Topped up btc_staking wallet (was $${STAKING_BALANCE} BTC)" + fi + # Check if any unconfirmed transactions were found or if the timeout has been reached if [ "$${TX_FOUND}" = true ] || [ $$(date +%s) -gt $${DEFAULT_TIMEOUT} ]; then if [ $$(date +%s) -gt $${DEFAULT_TIMEOUT} ]; then @@ -292,6 +312,7 @@ services: STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT STACKS_30_HEIGHT: *STACKS_30_HEIGHT + STACKS_35_HEIGHT: *STACKS_35_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH STACKS_CHAIN_ID: *STACKS_CHAIN_ID @@ -301,6 +322,41 @@ services: depends_on: - stacks-node + btc-staker: + networks: + - stacks + build: + context: . + dockerfile: Dockerfile.stacker + environment: + STACKS_CORE_RPC_HOST: stacks-node + STACKS_CORE_RPC_PORT: 20443 + STACKING_CYCLES: *STACKING_CYCLES + STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 + STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_30_HEIGHT: *STACKS_30_HEIGHT + STACKS_35_HEIGHT: *STACKS_35_HEIGHT + POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH + POX_REWARD_LENGTH: *POX_REWARD_LENGTH + STACKS_CHAIN_ID: *STACKS_CHAIN_ID + STACKING_INTERVAL: 2 + POST_TX_WAIT: 10 + SERVICE_NAME: btc-staker + BITCOIN_RPC_HOST: bitcoind + BITCOIN_RPC_PORT: *BITCOIN_RPC_PORT + BITCOIN_RPC_USER: *BITCOIN_RPC_USER + BITCOIN_RPC_PASS: *BITCOIN_RPC_PASS + BTC_LOCK_AMOUNT_SATS: 100000 + depends_on: + - stacks-node + - bitcoind + entrypoint: + - /bin/bash + - -c + - | + set -e + exec npx tsx /root/btc-staker.ts + monitor: networks: - stacks @@ -314,6 +370,7 @@ services: STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT STACKS_30_HEIGHT: *STACKS_30_HEIGHT + STACKS_35_HEIGHT: *STACKS_35_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH EXIT_FROM_MONITOR: *EXIT_FROM_MONITOR @@ -341,6 +398,7 @@ services: STACKS_30_HEIGHT: *STACKS_30_HEIGHT ACCOUNT_KEYS: 0d2f965b472a82efd5a96e6513c8b9f7edc725d5c96c7d35d6c722cedeb80d1b01,975b251dd7809469ef0c26ec3917971b75c51cd73a022024df4bf3b232cc2dc001,c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_35_HEIGHT: *STACKS_35_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH STACKS_CHAIN_ID: *STACKS_CHAIN_ID diff --git a/stacking/btc-locking.ts b/stacking/btc-locking.ts new file mode 100644 index 0000000..e69c404 --- /dev/null +++ b/stacking/btc-locking.ts @@ -0,0 +1,63 @@ +import * as BTC from '@scure/btc-signer'; +import { hex } from '@scure/base'; +import { createAddress } from '@stacks/transactions'; + +export const REGTEST_NETWORK = { + bech32: 'bcrt', + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, +} as const; + +// -- Script construction -- + +export function getUnlockBytes(pubKeyHex: string): Uint8Array { + return BTC.Script.encode([hex.decode(pubKeyHex), 'CHECKSIG']); +} + +export function serializeLockupScript({ + stacker, + unlockBurnHeight, + unlockBytes, +}: { + stacker: string; + unlockBurnHeight: bigint; + unlockBytes: Uint8Array; +}): Uint8Array { + const addr = createAddress(stacker); + return BTC.Script.encode([ + new Uint8Array([5, addr.version, ...hex.decode(addr.hash160)]), + 'DROP', + Number(unlockBurnHeight), + 'CHECKLOCKTIMEVERIFY', + 'DROP', + unlockBytes, + ]); +} + +export function toWitnessOutput(script: Uint8Array): Uint8Array { + return BTC.OutScript.encode(BTC.p2wsh({ type: 'wsh', script })); +} + +// -- Unlock height calculation -- + +export function calculateUnlockBurnHeight( + currentCycle: number, + numCycles: number, + rewardCycleLength: number, +): bigint { + const startCycle = currentCycle + 1; + const lastCycle = startCycle + numCycles - 1; + const lastCycleStartHeight = (lastCycle - 1) * rewardCycleLength; + return BigInt(lastCycleStartHeight + Math.floor(rewardCycleLength / 2)); +} + +// -- P2WSH address from lock script -- + +export function getLockingAddress(lockScript: Uint8Array): string { + const p2wsh = BTC.p2wsh({ + script: lockScript, + type: 'sh', + }, REGTEST_NETWORK); + return p2wsh.address!; +} diff --git a/stacking/btc-rpc.ts b/stacking/btc-rpc.ts new file mode 100644 index 0000000..e4c9cdb --- /dev/null +++ b/stacking/btc-rpc.ts @@ -0,0 +1,64 @@ +const host = process.env.BITCOIN_RPC_HOST ?? 'bitcoind'; +const port = process.env.BITCOIN_RPC_PORT ?? '18443'; +const user = process.env.BITCOIN_RPC_USER ?? 'btc'; +const pass = process.env.BITCOIN_RPC_PASS ?? 'btc'; + +const auth = 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64'); + +export async function bitcoinRPC( + method: string, + params: unknown[] = [], + wallet?: string, +): Promise { + const base = `http://${host}:${port}`; + const url = wallet ? `${base}/wallet/${wallet}` : base; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: auth }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), + }); + const json: any = await res.json(); + if (json.error) throw new Error(`bitcoinRPC ${method}: ${JSON.stringify(json.error)}`); + return json.result as T; +} + +export async function createOrLoadWallet(name: string) { + try { + await bitcoinRPC('createwallet', [name, false, false, '', false, false, true]); + } catch (e: any) { + if (!e.message.includes('already exists')) throw e; + } +} + +export function getNewAddress(wallet: string) { + return bitcoinRPC('getnewaddress', ['staking'], wallet); +} + +export interface Utxo { + txid: string; + vout: number; + address: string; + amount: number; + confirmations: number; + scriptPubKey: string; +} + +export function listUnspent(wallet: string, minConf = 1) { + return bitcoinRPC('listunspent', [minConf], wallet); +} + +export function getRawTransaction(txid: string): Promise { + return bitcoinRPC('getrawtransaction', [txid, false]); +} + +export function sendRawTransaction(hex: string) { + return bitcoinRPC('sendrawtransaction', [hex]); +} + +export function sendToAddress(wallet: string, address: string, amountBtc: number) { + return bitcoinRPC('sendtoaddress', [address, amountBtc], wallet); +} + +export function getBlockCount() { + return bitcoinRPC('getblockcount'); +} diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts new file mode 100644 index 0000000..3740b2e --- /dev/null +++ b/stacking/btc-staker.ts @@ -0,0 +1,260 @@ +import { + makeContractCall, + broadcastTransaction, + bufferCV, + uintCV, + tupleCV, + AnchorMode, + createAddress, +} from '@stacks/transactions'; +import { hex } from '@scure/base'; +import { Pox4SignatureTopic } from '@stacks/stacking'; +import { + accounts, + maxAmount, + parseEnvInt, + waitForSetup, + logger, + burnBlockToRewardCycle, + network, + POX_REWARD_LENGTH, + type Account, + EPOCH_35_START, + WALLET_NAME, +} from './common'; +import { createOrLoadWallet, getNewAddress, listUnspent, sendToAddress } from './btc-rpc'; +import { + getUnlockBytes, + serializeLockupScript, + calculateUnlockBurnHeight, + getLockingAddress, +} from './btc-locking'; + +const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); +const postTxWait = parseEnvInt('POST_TX_WAIT', true); +const stakingCycles = parseEnvInt('STACKING_CYCLES', true); +const lockAmountSats = BigInt(parseEnvInt('BTC_LOCK_AMOUNT_SATS', false) ?? 10_000_000); + +let txFee = parseEnvInt('STACKING_FEE', false) ?? 1_000_000; +const getNextTxFee = () => txFee++; + +// -- Initialization -- + +async function initBtcWallet() { + await createOrLoadWallet(WALLET_NAME); + logger.info({ wallet: WALLET_NAME }, 'Bitcoin staking wallet ready'); + + // Wait for miner to fund the wallet + while (true) { + const utxos = await listUnspent(WALLET_NAME, 1); + const total = utxos.reduce((sum, u) => sum + u.amount, 0); + if (total > 0) { + logger.info({ balance: total }, 'Staking wallet funded'); + return; + } + logger.info('Waiting for staking wallet to be funded...'); + await new Promise(r => setTimeout(r, 5000)); + } +} + +// -- L2: Stacks contract calls -- + +async function submitStake( + account: Account, + poxInfo: any, + unlockBytes: Uint8Array, +) { + const authId = Math.floor(Math.random() * 0xffffffffffff); + + const signerSignature = account.client.signPoxSignature({ + topic: Pox4SignatureTopic.StackStx, + rewardCycle: poxInfo.reward_cycle_id, + poxAddress: account.btcAddr, + period: stakingCycles, + signerPrivateKey: account.signerPrivKey, + authId, + maxAmount, + }); + + const poxAddr = createAddress(account.stxAddress); + const [contractAddr, contractName] = poxInfo.contract_id.split('.'); + + const txOptions = { + contractAddress: contractAddr, + contractName, + functionName: 'stake', + functionArgs: [ + // uintCV(account.balance!), + uintCV(100n), + tupleCV({ + version: bufferCV(Buffer.from([poxAddr.version])), + hashbytes: bufferCV(Buffer.from(hex.decode(poxAddr.hash160))), + }), + bufferCV(Buffer.from(hex.decode(signerSignature))), + bufferCV(Buffer.from(hex.decode(account.signerPubKey))), + uintCV(maxAmount), + uintCV(authId), + uintCV(stakingCycles), + bufferCV(Buffer.from(unlockBytes)), + uintCV(poxInfo.current_burnchain_block_height!), + ], + senderKey: account.privKey, + network, + fee: getNextTxFee(), + anchorMode: AnchorMode.Any, + }; + + const tx = await makeContractCall(txOptions); + const result = await broadcastTransaction(tx, network); + account.logger.info({ txid: result.txid }, 'L2 stake tx broadcast'); + return result; +} + +async function submitStakeExtend(account: Account, poxInfo: any, unlockBytes: Uint8Array) { + const authId = Math.floor(Math.random() * 0xffffffffffff); + + const signerSignature = account.client.signPoxSignature({ + topic: Pox4SignatureTopic.StackExtend, + rewardCycle: poxInfo.reward_cycle_id, + poxAddress: account.btcAddr, + period: stakingCycles, + signerPrivateKey: account.signerPrivKey, + authId, + maxAmount, + }); + + const [contractAddr, contractName] = poxInfo.contract_id.split('.'); + + const txOptions = { + contractAddress: contractAddr, + contractName, + functionName: 'stake-extend', + functionArgs: [ + uintCV(stakingCycles), + bufferCV(Buffer.from(unlockBytes)), + tupleCV({ + version: bufferCV(Buffer.from([createAddress(account.stxAddress).version])), + hashbytes: bufferCV( + Buffer.from(hex.decode(createAddress(account.stxAddress).hash160)), + ), + }), + bufferCV(Buffer.from(hex.decode(signerSignature))), + bufferCV(Buffer.from(hex.decode(account.signerPubKey))), + uintCV(maxAmount), + uintCV(authId), + ], + senderKey: account.privKey, + network, + fee: getNextTxFee(), + anchorMode: AnchorMode.Any, + }; + + const tx = await makeContractCall(txOptions); + const result = await broadcastTransaction(tx, network); + account.logger.info({ txid: result.txid }, 'L2 stake-extend tx broadcast'); + return result; +} + +// -- L1: Bitcoin locking transaction -- + +async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockBytes: Uint8Array) { + const lockScript = serializeLockupScript({ + stacker: account.stxAddress, + unlockBurnHeight, + unlockBytes, + }); + + const address = getLockingAddress(lockScript); + const amountBtc = Number(lockAmountSats) / 1e8; + + const txid = await sendToAddress(WALLET_NAME, address, amountBtc); + account.logger.info({ txid, address, amountBtc, unlockBurnHeight: unlockBurnHeight.toString() }, 'L1 BTC lock tx broadcast'); + return txid; +} + +// -- Main loop -- + +let lastStakedCycle = 0; + +async function run() { + const poxInfo = await accounts[0].client.getPoxInfo(); + if (poxInfo.current_burnchain_block_height! < EPOCH_35_START) { + logger.info({ burnHeight: poxInfo.current_burnchain_block_height }, 'Not on epoch 3.5 yet, skipping'); + return; + } + + const currentCycle = poxInfo.reward_cycle_id; + + const accountInfos = await Promise.all( + accounts.map(async a => { + const info = await a.client.getAccountStatus(); + return { + ...a, + unlockHeight: Number(info.unlock_height), + lockedAmount: BigInt(info.locked), + balance: BigInt(info.balance), + }; + }), + ); + + const nowCycle = burnBlockToRewardCycle(poxInfo.current_burnchain_block_height ?? 0); + + for (const account of accountInfos) { + const unlockBytes = getUnlockBytes(account.pubKey); + const unlockBurnHeight = calculateUnlockBurnHeight(currentCycle, stakingCycles, POX_REWARD_LENGTH); + + const ALWAYS_STAKE = true; // for now, testing + + if (ALWAYS_STAKE && nowCycle > lastStakedCycle) { + logger.info({ nowCycle, lastStakedCycle }, 'Staking through next cycle'); + // await submitStake(account, poxInfo, unlockBytes); + + await submitBtcLock(account, unlockBurnHeight, unlockBytes); + await new Promise(r => setTimeout(r, postTxWait * 1000)); + continue; + } + + // TODO: this won't trigger because we don't have pox-locking for pox-5 yet + + if (account.lockedAmount === 0n) { + account.logger.info('Account unlocked, staking...'); + + await submitStake(account, poxInfo, unlockBytes); + await new Promise(r => setTimeout(r, postTxWait * 1000)); + + await submitBtcLock(account, unlockBurnHeight, unlockBytes); + continue; + } + + const unlockCycle = burnBlockToRewardCycle(account.unlockHeight); + + if (unlockCycle === nowCycle + 1) { + account.logger.info({ unlockHeight: account.unlockHeight, nowCycle, unlockCycle }, 'Extending stake...'); + + await submitStakeExtend(account, poxInfo, unlockBytes); + await new Promise(r => setTimeout(r, postTxWait * 1000)); + + await submitBtcLock(account, unlockBurnHeight, unlockBytes); + continue; + } + + // account.logger.info({ nowCycle, unlockCycle }, 'Staked through next cycle, skipping'); + } + lastStakedCycle = nowCycle; +} + +async function loop() { + await waitForSetup(); + await initBtcWallet(); + + while (true) { + try { + await run(); + } catch (e) { + logger.error(e, 'Error in btc-staker loop'); + } + await new Promise(r => setTimeout(r, stakingInterval * 1000)); + } +} + +loop(); diff --git a/stacking/common.ts b/stacking/common.ts index fb9a949..34c5eac 100644 --- a/stacking/common.ts +++ b/stacking/common.ts @@ -54,8 +54,10 @@ export const accountsApi = new AccountsApi(apiConfig); export const EPOCH_30_START = parseEnvInt('STACKS_30_HEIGHT', true); export const EPOCH_25_START = parseEnvInt('STACKS_25_HEIGHT', true); +export const EPOCH_35_START = parseEnvInt('STACKS_35_HEIGHT', true); export const POX_PREPARE_LENGTH = parseEnvInt('POX_PREPARE_LENGTH', true); export const POX_REWARD_LENGTH = parseEnvInt('POX_REWARD_LENGTH', true); +export const WALLET_NAME = 'btc_staking'; export const accounts = process.env.STACKING_KEYS!.split(',').map((privKey, index) => { const pubKey = getPublicKeyFromPrivate(privKey); diff --git a/stacking/monitor.ts b/stacking/monitor.ts index 467aa37..040fc45 100644 --- a/stacking/monitor.ts +++ b/stacking/monitor.ts @@ -1,3 +1,4 @@ +import { bitcoinRPC } from './btc-rpc'; import { accounts, nodeUrl, @@ -8,6 +9,7 @@ import { parseEnvInt, txApi, logger, + WALLET_NAME, } from './common'; import { Transaction, ContractCallTransaction } from '@stacks/stacks-blockchain-api-types'; @@ -35,14 +37,20 @@ async function getTransactions(): Promise { }) as ContractCallTransaction[]; } +async function getBtcStakerBalance() { + const balance = await bitcoinRPC('getbalance', [], WALLET_NAME); + return balance; +} + async function getInfo() { let { client } = accounts[0]; - const [poxInfo, blockInfo, txs] = await Promise.all([ + const [poxInfo, blockInfo, txs, btcStakerBalance] = await Promise.all([ client.getPoxInfo(), blocksApi.getBlock({ heightOrHash: 'latest', }), getTransactions(), + getBtcStakerBalance(), ]); const { reward_cycle_id } = poxInfo; return { @@ -50,6 +58,7 @@ async function getInfo() { blockInfo, nextCycleId: reward_cycle_id + 1, txs, + btcStakerBalance, }; } @@ -95,6 +104,7 @@ async function loop() { rewardCycle: reward_cycle_id, lastBurnBlock: `${burnHeightTimeAgo.toFixed(0)}s ago`, burnHash: blockInfo.burn_block_hash, + btcStakerBalance: info.btcStakerBalance, }); if (current_burnchain_block_height && current_burnchain_block_height !== lastBurnHeight) { diff --git a/stacking/package.json b/stacking/package.json index fcc7be4..00939cc 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -11,6 +11,9 @@ "author": "", "license": "ISC", "dependencies": { + "@noble/curves": "^2.0.1", + "@scure/base": "^1.2.0", + "@scure/btc-signer": "^1.5.0", "@stacks/api": "6.11.4-pr.472091f.0", "@stacks/blockchain-api-client": "7.8.2", "@stacks/common": "6.11.4-pr.36558cf.0", From d00f945b2b36b8d2f18558a886354c7a7c587fb8 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:33:50 -0700 Subject: [PATCH 04/30] feat: add tsconfig, update the stacks.js V7 --- .vscode/settings.json | 3 + Dockerfile.stacker | 2 +- Dockerfile.stacks-node | 2 +- docker-compose.yml | 19 +- stacking/Clarigen.toml | 30 + stacking/Clarinet.toml | 16 + stacking/btc-staker.ts | 71 +- stacking/clarigen-types.ts | 729 +++++++++++ stacking/common.ts | 27 +- stacking/contracts.ts | 4 + stacking/contracts/pox-5.clar | 1146 +++++++++++++++++ stacking/deployments/default.simnet-plan.yaml | 74 ++ stacking/flood.ts | 39 +- stacking/monitor.ts | 31 +- stacking/package.json | 18 +- stacking/settings/Devnet.toml | 79 ++ stacking/stacking.ts | 4 +- stacking/tsconfig.json | 6 + stacking/tx-broadcaster.ts | 38 +- 19 files changed, 2238 insertions(+), 100 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 stacking/Clarigen.toml create mode 100644 stacking/Clarinet.toml create mode 100644 stacking/clarigen-types.ts create mode 100644 stacking/contracts.ts create mode 100644 stacking/contracts/pox-5.clar create mode 100644 stacking/deployments/default.simnet-plan.yaml create mode 100644 stacking/settings/Devnet.toml create mode 100644 stacking/tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..77eea67 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "biome.enabled": false +} diff --git a/Dockerfile.stacker b/Dockerfile.stacker index 46ef1e3..160981b 100644 --- a/Dockerfile.stacker +++ b/Dockerfile.stacker @@ -6,6 +6,6 @@ WORKDIR /root COPY ./stacking/package.json /root/ RUN npm i -COPY ./stacking/stacking.ts ./stacking/common.ts ./stacking/monitor.ts ./stacking/tx-broadcaster.ts ./stacking/btc-staker.ts ./stacking/btc-rpc.ts ./stacking/btc-locking.ts /root/ +COPY ./stacking/*.ts /root/ CMD ["npx", "tsx", "/root/stacking.ts"] \ No newline at end of file diff --git a/Dockerfile.stacks-node b/Dockerfile.stacks-node index 397b8e0..f7b7bb8 100644 --- a/Dockerfile.stacks-node +++ b/Dockerfile.stacks-node @@ -16,7 +16,7 @@ RUN git init && \ RUN --mount=type=cache,target=/stacks/target \ --mount=type=cache,target=/usr/local/cargo/registry \ - cargo build --package stacks-node --package stacks-signer --bin stacks-node --bin stacks-signer && \ + cargo build --package stacks-node --package stacks-signer --bin stacks-node --bin stacks-signer --features slog_json && \ cp target/debug/stacks-node target/debug/stacks-signer /usr/local/bin/ FROM debian:bookworm diff --git a/docker-compose.yml b/docker-compose.yml index fe62942..5d4e847 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,5 @@ -version: "3.9" - x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 3425ccdcdce92c4bd5e1efce5bef136b3e65c473 # feat/epoch-3-5-rc + - &STACKS_BLOCKCHAIN_COMMIT 59d0928bba3f401552ef65b6c0b79ac6dc4cd303 # feat/epoch-3-5-rc - &STACKS_API_COMMIT cb5881749553a34704cab62a9631341cb30f2c17 # 8.13.4 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za @@ -10,8 +8,9 @@ x-common-vars: - &BITCOIN_RPC_USER btc - &BITCOIN_RPC_PASS btc - &MINE_INTERVAL ${MINE_INTERVAL:-1s} - - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-5s} # 5 second bitcoin block times in epoch 2.5 - - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-30s} # 30 second bitcoin block times in epoch 3 + - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-2s} # 5 second bitcoin block times in epoch 2.5 + - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-3s} # 30 second bitcoin block times in epoch 3 + - &MINE_INTERVAL_EPOCH35 ${MINE_INTERVAL_EPOCH35:-20s} # 5 second bitcoin block times in epoch 3.5 - &NAKAMOTO_BLOCK_INTERVAL 2 # seconds to wait between issuing stx-transfer transactions (which triggers Nakamoto block production) - &STACKS_20_HEIGHT ${STACKS_20_HEIGHT:-0} - &STACKS_2_05_HEIGHT ${STACKS_2_05_HEIGHT:-102} @@ -26,7 +25,7 @@ x-common-vars: - &STACKS_32_HEIGHT ${STACKS_32_HEIGHT:-133} - &STACKS_33_HEIGHT ${STACKS_33_HEIGHT:-134} - &STACKS_34_HEIGHT ${STACKS_34_HEIGHT:-135} - - &STACKS_35_HEIGHT ${STACKS_35_HEIGHT:-136} + - &STACKS_35_HEIGHT ${STACKS_35_HEIGHT:-141} - &STACKING_CYCLES ${STACKING_CYCLES:-1} # number of cycles to stack-stx or stack-extend for - &POX_PREPARE_LENGTH ${POX_PREPARE_LENGTH:-5} - &POX_REWARD_LENGTH ${POX_REWARD_LENGTH:-20} @@ -77,9 +76,11 @@ services: MINE_INTERVAL: *MINE_INTERVAL MINE_INTERVAL_EPOCH3: *MINE_INTERVAL_EPOCH3 MINE_INTERVAL_EPOCH25: *MINE_INTERVAL_EPOCH25 + MINE_INTERVAL_EPOCH35: *MINE_INTERVAL_EPOCH35 INIT_BLOCKS: 101 STACKS_30_HEIGHT: *STACKS_30_HEIGHT STACKS_25_HEIGHT: *STACKS_25_HEIGHT + STACKS_35_HEIGHT: *STACKS_35_HEIGHT entrypoint: - /bin/bash - -c @@ -227,7 +228,10 @@ services: SLEEP_DURATION=$${MINE_INTERVAL} BLOCK_HEIGHT=$$(bitcoin-cli -rpcconnect=bitcoind getblockcount) - if [ "$${BLOCK_HEIGHT}" -ge $$(( $${STACKS_30_HEIGHT} - 1 )) ]; then + if [ "$${BLOCK_HEIGHT}" -ge "$${STACKS_35_HEIGHT}" ]; then + echo "In Epoch3.5, sleeping for $${MINE_INTERVAL_EPOCH35} ..." + SLEEP_DURATION=$${MINE_INTERVAL_EPOCH35} + elif [ "$${BLOCK_HEIGHT}" -ge $$(( $${STACKS_30_HEIGHT} - 1 )) ]; then echo "In Epoch3, sleeping for $${MINE_INTERVAL_EPOCH3} ..." SLEEP_DURATION=$${MINE_INTERVAL_EPOCH3} elif [ "$${BLOCK_HEIGHT}" -ge "$${STACKS_25_HEIGHT}" ]; then @@ -284,6 +288,7 @@ services: POX_REWARD_LENGTH: *POX_REWARD_LENGTH REWARD_RECIPIENT: *REWARD_RECIPIENT STACKS_CHAIN_ID: *STACKS_CHAIN_ID + STACKS_LOG_JSON: 1 entrypoint: - /bin/bash - -c diff --git a/stacking/Clarigen.toml b/stacking/Clarigen.toml new file mode 100644 index 0000000..3d0eea4 --- /dev/null +++ b/stacking/Clarigen.toml @@ -0,0 +1,30 @@ + +# Set to your project's Clarinet config file +clarinet = "./Clarinet.toml" + +# Set where you'd like TypeScript types output. +# Comment or remove section to skip TypeScript types +[types] +# `output` should be a path to a single file +output = "clarigen-types.ts" + +# You can also specify multiple output paths: +# outputs = [ +# "src/clarigen-types.ts", +# "test/clarigen-types.ts" +# ] + +# `types.after` - script to run after TypeScript types are generated. +# examples: +# after = "npm run prettier -w ./src/clarigen-types.ts" +# after = "echo 'yay'" + +# Set where you'd like generated contract docs +# Generate docs by running `clarigen docs` +[docs] +# `output` should be a folder +output = "docs" + +# `docs.after` - script to run after docs are generated. +# examples: +# after = "npm run prettier -w ./docs" diff --git a/stacking/Clarinet.toml b/stacking/Clarinet.toml new file mode 100644 index 0000000..e9563d4 --- /dev/null +++ b/stacking/Clarinet.toml @@ -0,0 +1,16 @@ + +[project] +name = "core-contracts" + +[repl] +costs_version = 1 + +[contracts.pox-5] +path = "./contracts/pox-5.clar" +clarity_version = 4 +epoch = 3.3 + +[repl.analysis.lints] +unused_const = "warn" +unused_data_var = "warn" +panic = "off" diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 3740b2e..5fce54c 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -6,9 +6,10 @@ import { tupleCV, AnchorMode, createAddress, + someCV, } from '@stacks/transactions'; import { hex } from '@scure/base'; -import { Pox4SignatureTopic } from '@stacks/stacking'; +import { Pox4SignatureTopic, PoxInfo } from '@stacks/stacking'; import { accounts, maxAmount, @@ -21,14 +22,16 @@ import { type Account, EPOCH_35_START, WALLET_NAME, -} from './common'; -import { createOrLoadWallet, getNewAddress, listUnspent, sendToAddress } from './btc-rpc'; +} from './common.js'; +import { createOrLoadWallet, getNewAddress, listUnspent, sendToAddress } from './btc-rpc.js'; import { getUnlockBytes, serializeLockupScript, calculateUnlockBurnHeight, getLockingAddress, -} from './btc-locking'; +} from './btc-locking.js'; +import { pox5 } from './contracts.js'; +import { TESTNET_BURN_ADDRESS } from '@clarigen/core'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); const postTxWait = parseEnvInt('POST_TX_WAIT', true); @@ -61,7 +64,7 @@ async function initBtcWallet() { async function submitStake( account: Account, - poxInfo: any, + poxInfo: PoxInfo, unlockBytes: Uint8Array, ) { const authId = Math.floor(Math.random() * 0xffffffffffff); @@ -77,26 +80,42 @@ async function submitStake( }); const poxAddr = createAddress(account.stxAddress); - const [contractAddr, contractName] = poxInfo.contract_id.split('.'); + const [contractAddr] = poxInfo.contract_id.split('.'); + + const stakeFnCall = pox5.stake({ + amountUstx: 100n, + poxAddr: { + version: Buffer.from([poxAddr.version]), + hashbytes: Buffer.from(hex.decode(poxAddr.hash160)), + }, + startBurnHt: poxInfo.current_burnchain_block_height!, + signerSig: Buffer.from(hex.decode(signerSignature)), + signerKey: Buffer.from(hex.decode(account.signerPubKey)), + maxAmount, + authId, + numCycles: stakingCycles, + unlockBytes: unlockBytes, + }); const txOptions = { - contractAddress: contractAddr, - contractName, + // ...stakeFnCall, + contractAddress: TESTNET_BURN_ADDRESS, + contractName: 'pox-5', functionName: 'stake', functionArgs: [ // uintCV(account.balance!), uintCV(100n), tupleCV({ - version: bufferCV(Buffer.from([poxAddr.version])), - hashbytes: bufferCV(Buffer.from(hex.decode(poxAddr.hash160))), + version: bufferCV(new Uint8Array([1])), + hashbytes: bufferCV(hex.decode(poxAddr.hash160)), }), - bufferCV(Buffer.from(hex.decode(signerSignature))), - bufferCV(Buffer.from(hex.decode(account.signerPubKey))), + uintCV(poxInfo.current_burnchain_block_height!), + someCV(bufferCV(hex.decode(signerSignature))), + bufferCV(hex.decode(account.signerPubKey)), uintCV(maxAmount), uintCV(authId), uintCV(stakingCycles), bufferCV(Buffer.from(unlockBytes)), - uintCV(poxInfo.current_burnchain_block_height!), ], senderKey: account.privKey, network, @@ -105,8 +124,11 @@ async function submitStake( }; const tx = await makeContractCall(txOptions); - const result = await broadcastTransaction(tx, network); - account.logger.info({ txid: result.txid }, 'L2 stake tx broadcast'); + const result = await broadcastTransaction({ + transaction: tx, + network, + }); + account.logger.info({ ...result }, 'L2 stake tx broadcast'); return result; } @@ -150,7 +172,10 @@ async function submitStakeExtend(account: Account, poxInfo: any, unlockBytes: Ui }; const tx = await makeContractCall(txOptions); - const result = await broadcastTransaction(tx, network); + const result = await broadcastTransaction({ + transaction: tx, + network, + }); account.logger.info({ txid: result.txid }, 'L2 stake-extend tx broadcast'); return result; } @@ -177,9 +202,9 @@ async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockB let lastStakedCycle = 0; async function run() { - const poxInfo = await accounts[0].client.getPoxInfo(); + const poxInfo = await accounts[0]!.client.getPoxInfo(); if (poxInfo.current_burnchain_block_height! < EPOCH_35_START) { - logger.info({ burnHeight: poxInfo.current_burnchain_block_height }, 'Not on epoch 3.5 yet, skipping'); + // logger.info({ burnHeight: poxInfo.current_burnchain_block_height }, 'Not on epoch 3.5 yet, skipping'); return; } @@ -203,13 +228,16 @@ async function run() { const unlockBytes = getUnlockBytes(account.pubKey); const unlockBurnHeight = calculateUnlockBurnHeight(currentCycle, stakingCycles, POX_REWARD_LENGTH); - const ALWAYS_STAKE = true; // for now, testing + const ALWAYS_STAKE = false; // for now, testing if (ALWAYS_STAKE && nowCycle > lastStakedCycle) { logger.info({ nowCycle, lastStakedCycle }, 'Staking through next cycle'); // await submitStake(account, poxInfo, unlockBytes); await submitBtcLock(account, unlockBurnHeight, unlockBytes); + if (account.lockedAmount === 0n) { + await submitStake(account, poxInfo, unlockBytes); + } await new Promise(r => setTimeout(r, postTxWait * 1000)); continue; } @@ -217,7 +245,10 @@ async function run() { // TODO: this won't trigger because we don't have pox-locking for pox-5 yet if (account.lockedAmount === 0n) { - account.logger.info('Account unlocked, staking...'); + account.logger.info('Account unlocked, staking...', { + account: account.index, + rewardCycle: poxInfo.reward_cycle_id, + }); await submitStake(account, poxInfo, unlockBytes); await new Promise(r => setTimeout(r, postTxWait * 1000)); diff --git a/stacking/clarigen-types.ts b/stacking/clarigen-types.ts new file mode 100644 index 0000000..95af735 --- /dev/null +++ b/stacking/clarigen-types.ts @@ -0,0 +1,729 @@ + +import type { TypedAbiArg, TypedAbiFunction, TypedAbiMap, TypedAbiVariable, Response } from '@clarigen/core'; + +export const contracts = { + pox5: { + "functions": { + addStakerToNthRewardCycle: {"name":"add-staker-to-nth-reward-cycle","access":"private","args":[{"name":"cycle-index","type":"uint128"},{"name":"params-resp","type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[cycleIndex: TypedAbiArg, paramsResp: TypedAbiArg, "paramsResp">], Response<{ + "firstRewardCycle": bigint; + "staker": string; +}, bigint>>, + addStakerToRewardCycles: {"name":"add-staker-to-reward-cycles","access":"private","args":[{"name":"staker","type":"principal"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstRewardCycle: TypedAbiArg, numCycles: TypedAbiArg], Response>, + addStakerToSetForCycle: {"name":"add-staker-to-set-for-cycle","access":"private","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], Response>, + consumeSignerKeyAuthorization: {"name":"consume-signer-key-authorization","access":"private","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"amount","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, signerSig: TypedAbiArg, signerKey: TypedAbiArg, amount: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Response>, + innerStake: {"name":"inner-stake","access":"private","args":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"start-burn-ht","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg, startBurnHt: TypedAbiArg, poolOrSoloInfo: TypedAbiArg, "poolOrSoloInfo">], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + innerStakeExtend: {"name":"inner-stake-extend","access":"private","args":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg, poolOrSoloInfo: TypedAbiArg, "poolOrSoloInfo">], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + innerStakeUpdate: {"name":"inner-stake-update","access":"private","args":[{"name":"amount-ustx-increase","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstxIncrease: TypedAbiArg, poolOrSoloInfo: TypedAbiArg, "poolOrSoloInfo">], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + validateSignerKeyUsage: {"name":"validate-signer-key-usage","access":"private","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"signer-sig-opt","type":{"optional":{"buffer":{"length":65}}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"amount","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"},{"name":"staker","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, signerSigOpt: TypedAbiArg, signerKey: TypedAbiArg, amount: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg, staker: TypedAbiArg], Response>, + grantSignerKey: {"name":"grant-signer-key","access":"public","args":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"staker","type":"principal"},{"name":"pox-addr","type":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}},{"name":"auth-id","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerKey: TypedAbiArg, staker: TypedAbiArg, poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +} | null, "poxAddr">, authId: TypedAbiArg, signerSig: TypedAbiArg], Response>, + registerPool: {"name":"register-pool","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-sig","type":{"buffer":{"length":65}}},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"owner","type":"principal"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, signerKey: TypedAbiArg, poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, signerSig: TypedAbiArg, authId: TypedAbiArg], Response<{ + "owner": string; + "poxAddr": { + "hashbytes": Uint8Array; + "version": Uint8Array; +}; + "signerKey": Uint8Array; +}, bigint>>, + revokeSignerGrant: {"name":"revoke-signer-grant","access":"public","args":[{"name":"staker","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, signerKey: TypedAbiArg], Response>, + setBurnchainParameters: {"name":"set-burnchain-parameters","access":"public","args":[{"name":"first-burn-height","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"begin-pox5-reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[firstBurnHeight: TypedAbiArg, prepareCycleLength: TypedAbiArg, rewardCycleLength: TypedAbiArg, beginPox5RewardCycle: TypedAbiArg], Response>, + stake: {"name":"stake","access":"public","args":[{"name":"amount-ustx","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"start-burn-ht","type":"uint128"},{"name":"signer-sig","type":{"optional":{"buffer":{"length":65}}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, startBurnHt: TypedAbiArg, signerSig: TypedAbiArg, signerKey: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + stakeExtend: {"name":"stake-extend","access":"public","args":[{"name":"amount-ustx","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-sig","type":{"optional":{"buffer":{"length":65}}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, signerSig: TypedAbiArg, signerKey: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + stakeExtendPooled: {"name":"stake-extend-pooled","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + stakePooled: {"name":"stake-pooled","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"start-burn-ht","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg, startBurnHt: TypedAbiArg], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + stakeUpdate: {"name":"stake-update","access":"public","args":[{"name":"amount-ustx-increase","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-sig","type":{"optional":{"buffer":{"length":65}}}},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstxIncrease: TypedAbiArg, poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, signerKey: TypedAbiArg, signerSig: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + stakeUpdatePooled: {"name":"stake-update-pooled","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"amount-ustx-increase","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, amountUstxIncrease: TypedAbiArg], Response<{ + "amountUstx": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "stacker": string; + "unlockBurnHeight": bigint; + "unlockBytes": Uint8Array; + "unlockCycle": bigint; +}, bigint>>, + burnHeightToRewardCycle: {"name":"burn-height-to-reward-cycle","access":"read_only","args":[{"name":"height","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[height: TypedAbiArg], bigint>, + checkPoxAddr: {"name":"check-pox-addr","access":"read_only","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">], Response>, + checkPoxAddrHashbytes: {"name":"check-pox-addr-hashbytes","access":"read_only","args":[{"name":"version","type":{"buffer":{"length":1}}},{"name":"hashbytes","type":{"buffer":{"length":32}}}],"outputs":{"type":"bool"}} as TypedAbiFunction<[version: TypedAbiArg, hashbytes: TypedAbiArg], boolean>, + checkPoxAddrVersion: {"name":"check-pox-addr-version","access":"read_only","args":[{"name":"version","type":{"buffer":{"length":1}}}],"outputs":{"type":"bool"}} as TypedAbiFunction<[version: TypedAbiArg], boolean>, + checkPoxLockPeriod: {"name":"check-pox-lock-period","access":"read_only","args":[{"name":"lock-period","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[lockPeriod: TypedAbiArg], boolean>, + currentPoxRewardCycle: {"name":"current-pox-reward-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getPoolInfo: {"name":"get-pool-info","access":"read_only","args":[{"name":"owner","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}} as TypedAbiFunction<[owner: TypedAbiArg], { + "poxAddr": { + "hashbytes": Uint8Array; + "version": Uint8Array; +}; + "signerKey": Uint8Array; +} | null>, + getPoxInfo: {"name":"get-pox-info","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-burnchain-block-height","type":"uint128"},{"name":"min-amount-ustx","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-id","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"total-liquid-supply-ustx","type":"uint128"}]},"error":"none"}}}} as TypedAbiFunction<[], Response<{ + "firstBurnchainBlockHeight": bigint; + "minAmountUstx": bigint; + "prepareCycleLength": bigint; + "rewardCycleId": bigint; + "rewardCycleLength": bigint; + "totalLiquidSupplyUstx": bigint; +}, null>>, + getSignerGrantMessageHash: {"name":"get-signer-grant-message-hash","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"pox-addr","type":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[staker: TypedAbiArg, poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +} | null, "poxAddr">, authId: TypedAbiArg], Uint8Array>, + getSignerKeyMessageHash: {"name":"get-signer-key-message-hash","access":"read_only","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Uint8Array>, + getStakerInfo: {"name":"get-staker-info","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { + "amountUstx": bigint; + "firstRewardCycle": bigint; + "numCycles": bigint; + "poolOrSoloInfo": Response; + "unlockBytes": Uint8Array; +} | null>, + getStakerSetFirstItemForCycle: {"name":"get-staker-set-first-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, + getStakerSetItemForCycle: {"name":"get-staker-set-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], { + "next": string | null; + "prev": string | null; +} | null>, + getStakerSetLastItemForCycle: {"name":"get-staker-set-last-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, + getStakerSetNextItemForCycle: {"name":"get-staker-set-next-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], string | null>, + getStakerSetPrevItemForCycle: {"name":"get-staker-set-prev-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], string | null>, + rewardCycleToBurnHeight: {"name":"reward-cycle-to-burn-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, + rewardCycleToUnlockHeight: {"name":"reward-cycle-to-unlock-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, + stakerSetContainsForCycle: {"name":"staker-set-contains-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], boolean>, + verifySignerKeyGrant: {"name":"verify-signer-key-grant","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, signerKey: TypedAbiArg, poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">], Response>, + verifySignerKeySig: {"name":"verify-signer-key-sig","access":"read_only","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"amount","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, signerSig: TypedAbiArg, signerKey: TypedAbiArg, amount: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Response> + }, + "maps": { + pools: {"name":"pools","key":"principal","value":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}} as TypedAbiMap, + signerKeyGrants: {"name":"signer-key-grants","key":{"tuple":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"staker","type":"principal"}]},"value":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}} as TypedAbiMap<{ + "signerKey": Uint8Array; + "staker": string; +}, { + "hashbytes": Uint8Array; + "version": Uint8Array; +} | null>, + stakerSetLlFirstForCycle: {"name":"staker-set-ll-first-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, + stakerSetLlForCycle: {"name":"staker-set-ll-for-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"value":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}} as TypedAbiMap<{ + "cycle": number | bigint; + "staker": string; +}, { + "next": string | null; + "prev": string | null; +}>, + stakerSetLlLastForCycle: {"name":"staker-set-ll-last-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, + stakingState: {"name":"staking-state","key":"principal","value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]}} as TypedAbiMap; + "unlockBytes": Uint8Array; +}>, + usedSignerKeyAuthorizations: {"name":"used-signer-key-authorizations","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"period","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"topic","type":{"string-ascii":{"length":14}}}]},"value":"bool"} as TypedAbiMap<{ + "authId": number | bigint; + "maxAmount": number | bigint; + "period": number | bigint; + "poxAddr": { + "hashbytes": Uint8Array; + "version": Uint8Array; +}; + "rewardCycle": number | bigint; + "signerKey": Uint8Array; + "topic": string; +}, boolean>, + usedSignerKeyGrants: {"name":"used-signer-key-grants","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"staker","type":"principal"}]},"value":"bool"} as TypedAbiMap<{ + "authId": number | bigint; + "signerKey": Uint8Array; + "staker": string; +}, boolean> + }, + "variables": { + ERR_ALREADY_STAKED: { + name: 'ERR_ALREADY_STAKED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_CANNOT_EXTEND: { + name: 'ERR_CANNOT_EXTEND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INSUFFICIENT_FUNDS: { + name: 'ERR_INSUFFICIENT_FUNDS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_AMOUNT: { + name: 'ERR_INVALID_AMOUNT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_NUM_CYCLES: { + name: 'ERR_INVALID_NUM_CYCLES', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_POX_ADDRESS: { + name: 'ERR_INVALID_POX_ADDRESS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_SIGNATURE_PUBKEY: { + name: 'ERR_INVALID_SIGNATURE_PUBKEY', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_SIGNATURE_RECOVER: { + name: 'ERR_INVALID_SIGNATURE_RECOVER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_START_BURN_HEIGHT: { + name: 'ERR_INVALID_START_BURN_HEIGHT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_ALLOWED: { + name: 'ERR_NOT_ALLOWED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_STAKED: { + name: 'ERR_NOT_STAKED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_POOL_NOT_FOUND: { + name: 'ERR_POOL_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { + name: 'ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_AUTH_USED: { + name: 'ERR_SIGNER_AUTH_USED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_NOT_FOUND: { + name: 'ERR_SIGNER_KEY_GRANT_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { + name: 'ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_USED: { + name: 'ERR_SIGNER_KEY_GRANT_USED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + MAX_ADDRESS_VERSION: { + name: 'MAX_ADDRESS_VERSION', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + mAX_ADDRESS_VERSION_BUFF_20: { + name: 'MAX_ADDRESS_VERSION_BUFF_20', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + mAX_ADDRESS_VERSION_BUFF_32: { + name: 'MAX_ADDRESS_VERSION_BUFF_32', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + MAX_NUM_CYCLES: { + name: 'MAX_NUM_CYCLES', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + MIN_STACKING_AMOUNT: { + name: 'MIN_STACKING_AMOUNT', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + pOX_5_SIGNER_DOMAIN: { + name: 'POX_5_SIGNER_DOMAIN', + type: { + tuple: [ + { + name: 'chain-id', + type: 'uint128' + }, + { + name: 'name', + type: { + 'string-ascii': { + length: 12 + } + } + }, + { + name: 'version', + type: { + 'string-ascii': { + length: 5 + } + } + } + ] + }, + access: 'constant' +} as TypedAbiVariable<{ + "chainId": bigint; + "name": string; + "version": string; +}>, + PREPARE_CYCLE_LENGTH: { + name: 'PREPARE_CYCLE_LENGTH', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + REWARD_CYCLE_LENGTH: { + name: 'REWARD_CYCLE_LENGTH', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + sIP018_MSG_PREFIX: { + name: 'SIP018_MSG_PREFIX', + type: { + buffer: { + length: 6 + } + }, + access: 'constant' +} as TypedAbiVariable, + STACKS_ADDR_VERSION_MAINNET: { + name: 'STACKS_ADDR_VERSION_MAINNET', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + STACKS_ADDR_VERSION_TESTNET: { + name: 'STACKS_ADDR_VERSION_TESTNET', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + configured: { + name: 'configured', + type: 'bool', + access: 'variable' +} as TypedAbiVariable, + firstBurnchainBlockHeight: { + name: 'first-burnchain-block-height', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + firstPox5RewardCycle: { + name: 'first-pox-5-reward-cycle', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + poxPrepareCycleLength: { + name: 'pox-prepare-cycle-length', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + poxRewardCycleLength: { + name: 'pox-reward-cycle-length', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable + }, + constants: { + ERR_ALREADY_STAKED: { + isOk: false, + value: 1n + }, + ERR_CANNOT_EXTEND: { + isOk: false, + value: 10n + }, + ERR_INSUFFICIENT_FUNDS: { + isOk: false, + value: 4n + }, + ERR_INVALID_AMOUNT: { + isOk: false, + value: 11n + }, + ERR_INVALID_NUM_CYCLES: { + isOk: false, + value: 9n + }, + ERR_INVALID_POX_ADDRESS: { + isOk: false, + value: 13n + }, + ERR_INVALID_SIGNATURE_PUBKEY: { + isOk: false, + value: 17n + }, + ERR_INVALID_SIGNATURE_RECOVER: { + isOk: false, + value: 16n + }, + ERR_INVALID_START_BURN_HEIGHT: { + isOk: false, + value: 8n + }, + ERR_NOT_ALLOWED: { + isOk: false, + value: 23n + }, + ERR_NOT_STAKED: { + isOk: false, + value: 2n + }, + ERR_POOL_NOT_FOUND: { + isOk: false, + value: 14n + }, + ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { + isOk: false, + value: 19n + }, + ERR_SIGNER_AUTH_USED: { + isOk: false, + value: 20n + }, + ERR_SIGNER_KEY_GRANT_NOT_FOUND: { + isOk: false, + value: 21n + }, + ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { + isOk: false, + value: 22n + }, + ERR_SIGNER_KEY_GRANT_USED: { + isOk: false, + value: 15n + }, + MAX_ADDRESS_VERSION: 6n, + mAX_ADDRESS_VERSION_BUFF_20: 4n, + mAX_ADDRESS_VERSION_BUFF_32: 6n, + MAX_NUM_CYCLES: 24n, + MIN_STACKING_AMOUNT: 100_000_000n, + pOX_5_SIGNER_DOMAIN: { + chainId: 2_147_483_648n, + name: 'pox-5-signer', + version: '1.0.0' + }, + PREPARE_CYCLE_LENGTH: 50n, + REWARD_CYCLE_LENGTH: 1_050n, + sIP018_MSG_PREFIX: Uint8Array.from([83,73,80,48,49,56]), + STACKS_ADDR_VERSION_MAINNET: Uint8Array.from([22]), + STACKS_ADDR_VERSION_TESTNET: Uint8Array.from([26]), + configured: false, + firstBurnchainBlockHeight: 0n, + firstPox5RewardCycle: 0n, + poxPrepareCycleLength: 50n, + poxRewardCycleLength: 1_050n +}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch33","clarity_version":"Clarity4", + contractName: 'pox-5', + } +} as const; + +export const accounts = {"deployer":{"address":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","balance":"100000000000000"},"wallet_1":{"address":"ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5","balance":"100000000000000"},"wallet_10":{"address":"ST3FFKYTTB975A3JC3F99MM7TXZJ406R3GKE6JV56","balance":"200000000000000"},"wallet_2":{"address":"ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG","balance":"100000000000000"},"wallet_3":{"address":"ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC","balance":"100000000000000"},"wallet_4":{"address":"ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND","balance":"100000000000000"},"wallet_5":{"address":"ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB","balance":"100000000000000"},"wallet_6":{"address":"ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0","balance":"100000000000000"},"wallet_7":{"address":"ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ","balance":"100000000000000"},"wallet_8":{"address":"ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP","balance":"100000000000000"},"wallet_9":{"address":"STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6","balance":"100000000000000"}} as const; + +export const identifiers = {"pox5":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5"} as const + +export const simnet = { + accounts, + contracts, + identifiers, +} as const; + + +export const deployments = {"pox5":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","simnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","testnet":null,"mainnet":null}} as const; + +export const project = { + contracts, + deployments, +} as const; + \ No newline at end of file diff --git a/stacking/common.ts b/stacking/common.ts index 34c5eac..e52f79c 100644 --- a/stacking/common.ts +++ b/stacking/common.ts @@ -1,9 +1,7 @@ import { StackingClient } from '@stacks/stacking'; -import { StacksTestnet } from '@stacks/network'; +import { STACKS_TESTNET } from '@stacks/network'; import { getAddressFromPrivateKey, - TransactionVersion, - createStacksPrivateKey, } from '@stacks/transactions'; import { getPublicKeyFromPrivate, publicKeyToBtcAddress } from '@stacks/encryption'; import { @@ -14,8 +12,7 @@ import { SmartContractsApi, AccountsApi, } from '@stacks/blockchain-api-client'; -import pino, { Logger } from 'pino'; -import { ChainID } from '@stacks/common'; +import { Logger, pino } from 'pino'; const serviceName = process.env.SERVICE_NAME || 'JS'; export let logger: Logger; @@ -38,11 +35,12 @@ if (process.env.STACKS_LOG_JSON === '1') { }); } -export const CHAIN_ID = parseEnvInt('STACKS_CHAIN_ID', false) ?? ChainID.Testnet; +export const CHAIN_ID = parseEnvInt('STACKS_CHAIN_ID', false) ?? STACKS_TESTNET.chainId; export const nodeUrl = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`; -export const network = new StacksTestnet({ url: nodeUrl }); +export const network = STACKS_TESTNET; network.chainId = CHAIN_ID; +network.client.baseUrl = nodeUrl; const apiConfig = new Configuration({ basePath: nodeUrl, }); @@ -61,9 +59,9 @@ export const WALLET_NAME = 'btc_staking'; export const accounts = process.env.STACKING_KEYS!.split(',').map((privKey, index) => { const pubKey = getPublicKeyFromPrivate(privKey); - const stxAddress = getAddressFromPrivateKey(privKey, TransactionVersion.Testnet); - const signerPrivKey = createStacksPrivateKey(privKey); - const signerPubKey = getPublicKeyFromPrivate(signerPrivKey.data); + const stxAddress = getAddressFromPrivateKey(privKey, network); + const signerPrivKey = privKey; + const signerPubKey = getPublicKeyFromPrivate(signerPrivKey); return { privKey, pubKey, @@ -73,7 +71,10 @@ export const accounts = process.env.STACKING_KEYS!.split(',').map((privKey, inde signerPubKey: signerPubKey, targetSlots: index + 1, index, - client: new StackingClient(stxAddress, network), + client: new StackingClient({ + address: stxAddress, + network, + }), logger: logger.child({ account: stxAddress, index: index, @@ -88,9 +89,9 @@ export const maxAmount = MAX_U128; export async function waitForSetup() { try { - await accounts[0].client.getPoxInfo(); + await accounts[0]!.client.getPoxInfo(); } catch (error) { - if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) { + if (error instanceof Error && 'cause' in error && error.cause instanceof Error && /(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause.message)) { console.log(`Stacks node not ready, waiting...`); } await new Promise(resolve => setTimeout(resolve, 3000)); diff --git a/stacking/contracts.ts b/stacking/contracts.ts new file mode 100644 index 0000000..34de27b --- /dev/null +++ b/stacking/contracts.ts @@ -0,0 +1,4 @@ +import { projectFactory, contractFactory, TESTNET_BURN_ADDRESS } from '@clarigen/core'; +import { project, contracts } from './clarigen-types.js'; + +export const pox5 = contractFactory(contracts.pox5, `${TESTNET_BURN_ADDRESS}.pox-5`); \ No newline at end of file diff --git a/stacking/contracts/pox-5.clar b/stacking/contracts/pox-5.clar new file mode 100644 index 0000000..ef700bd --- /dev/null +++ b/stacking/contracts/pox-5.clar @@ -0,0 +1,1146 @@ +;; The caller is already staked +(define-constant ERR_ALREADY_STAKED (err u1)) +;; The caller is not staked +(define-constant ERR_NOT_STAKED (err u2)) +;; The caller does not have sufficient STX to stake +(define-constant ERR_INSUFFICIENT_FUNDS (err u4)) +;; The `start-burn-ht` is not valid - it must be in the next reward cycle +(define-constant ERR_INVALID_START_BURN_HEIGHT (err u8)) +;; The `num-cycles` provided is invalid - it must be less than MAX_NUM_CYCLES +(define-constant ERR_INVALID_NUM_CYCLES (err u9)) +;; The stacker tried to call `stake-extend` but not during their last cycle +(define-constant ERR_CANNOT_EXTEND (err u10)) +(define-constant ERR_INVALID_AMOUNT (err u11)) +(define-constant ERR_INVALID_POX_ADDRESS (err u13)) +(define-constant ERR_POOL_NOT_FOUND (err u14)) +;; The signer key grant has already been used +(define-constant ERR_SIGNER_KEY_GRANT_USED (err u15)) +(define-constant ERR_INVALID_SIGNATURE_RECOVER (err u16)) +(define-constant ERR_INVALID_SIGNATURE_PUBKEY (err u17)) +(define-constant ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH (err u19)) +(define-constant ERR_SIGNER_AUTH_USED (err u20)) +(define-constant ERR_SIGNER_KEY_GRANT_NOT_FOUND (err u21)) +(define-constant ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH (err u22)) +(define-constant ERR_NOT_ALLOWED (err u23)) + +(define-trait pool-owner-trait ( + (validate-stake! + ;; caller, amount-ustx, num-cycles, unlock-bytes + (principal uint uint (buff 683)) + (response bool uint) + ) + (validate-management! + ;; caller, signer-key, pox-addr + (principal (buff 33) { + version: (buff 1), + hashbytes: (buff 32), + }) + (response bool uint) + ) +)) + +;; Values for stacks address versions +;; #[allow(unused_const)] +(define-constant STACKS_ADDR_VERSION_MAINNET 0x16) +;; #[allow(unused_const)] +(define-constant STACKS_ADDR_VERSION_TESTNET 0x1a) + +;; Maximum number of cycles you can stake for +(define-constant MAX_NUM_CYCLES u24) + +;; Minimum amount of uSTX you can stake +(define-constant MIN_STACKING_AMOUNT u100000000) ;; 100 STX + +;; SIP18 message prefix +(define-constant SIP018_MSG_PREFIX 0x534950303138) + +;; SIP018 domain +(define-constant POX_5_SIGNER_DOMAIN { + name: "pox-5-signer", + version: "1.0.0", + chain-id: chain-id, +}) + +;; Keep these constants in lock-step with the address version buffs above +;; Maximum value of an address version as a uint +(define-constant MAX_ADDRESS_VERSION u6) +;; Maximum value of an address version that has a 20-byte hashbytes +;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4) +;; Maximum value of an address version that has a 32-byte hashbytes +;; (0x05 and 0x06 have 32-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_32 u6) + +;; Default length of the PoX registration window, in burnchain blocks. +(define-constant PREPARE_CYCLE_LENGTH (if is-in-mainnet + u100 + u50 +)) + +;; Default length of the PoX reward cycle, in burnchain blocks. +(define-constant REWARD_CYCLE_LENGTH (if is-in-mainnet + u2100 + u1050 +)) + +;; Data vars that store a copy of the burnchain configuration. +;; Implemented as data-vars, so that different configurations can be +;; used in e.g. test harnesses. +;; #[allow(unused_data_var)] +(define-data-var pox-prepare-cycle-length uint PREPARE_CYCLE_LENGTH) +(define-data-var pox-reward-cycle-length uint REWARD_CYCLE_LENGTH) +(define-data-var first-burnchain-block-height uint u0) +(define-data-var configured bool false) +;; #[allow(unused_data_var)] +(define-data-var first-pox-5-reward-cycle uint u0) + +;; This function can only be called once, when it boots up +(define-public (set-burnchain-parameters + (first-burn-height uint) + (prepare-cycle-length uint) + (reward-cycle-length uint) + (begin-pox5-reward-cycle uint) + ) + (begin + (unwrap-panic (if (var-get configured) + (err false) + (ok true) + )) + (var-set first-burnchain-block-height first-burn-height) + (var-set pox-prepare-cycle-length prepare-cycle-length) + (var-set pox-reward-cycle-length reward-cycle-length) + (var-set first-pox-5-reward-cycle begin-pox5-reward-cycle) + (var-set configured true) + (ok true) + ) +) + +;; Users can stake to a pool, where the pool owner +;; (which is the key of this map) is able to manage +;; the signer key and pox address for the pool. +(define-map pools + principal + { + signer-key: (buff 33), + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + } +) + +(define-map staking-state + principal + { + num-cycles: uint, + unlock-bytes: (buff 683), + amount-ustx: uint, + first-reward-cycle: uint, + pool-or-solo-info: (response principal { + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + signer-key: (buff 33), + }), + } +) + +(define-map signer-key-grants + { + signer-key: (buff 33), + staker: principal, + } + (optional { + version: (buff 1), + hashbytes: (buff 32), + }) +) + +(define-map used-signer-key-grants + { + signer-key: (buff 33), + staker: principal, + auth-id: uint, + } + bool +) + +;; State for tracking used signer key authorizations. This prevents re-use +;; of the same signature or pre-set authorization for multiple transactions. +;; Refer to the `signer-key-authorizations` map for the documentation on these fields +(define-map used-signer-key-authorizations + { + signer-key: (buff 33), + reward-cycle: uint, + period: uint, + topic: (string-ascii 14), + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + auth-id: uint, + max-amount: uint, + } + bool ;; Whether the field has been used or not +) + +;; What's the reward cycle number of the burnchain block height? +;; Will runtime-abort if height is less than the first burnchain block (this is intentional) +(define-read-only (burn-height-to-reward-cycle (height uint)) + (/ (- height (var-get first-burnchain-block-height)) + (var-get pox-reward-cycle-length) + ) +) + +;; What's the block height at the start of a given reward cycle? +(define-read-only (reward-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) + (* cycle (var-get pox-reward-cycle-length)) + ) +) + +;; Get the L1 unlock height for a given reward cycle. +;; This is equal to exactly halfway through the provided cycle. +(define-read-only (reward-cycle-to-unlock-height (cycle uint)) + (+ (reward-cycle-to-burn-height cycle) + (/ (var-get pox-reward-cycle-length) u2) + ) +) + +;; What's the current PoX reward cycle? +(define-read-only (current-pox-reward-cycle) + (burn-height-to-reward-cycle burn-block-height) +) + +;; Get the _current_ PoX staking principal information. If the information +;; is expired, or if there's never been such a staker, then returns none. +(define-read-only (get-staker-info (staker principal)) + (match (map-get? staking-state staker) + staking-info + (if (<= + (+ (get first-reward-cycle staking-info) + (get num-cycles staking-info) + ) + (current-pox-reward-cycle) + ) + ;; present, but lock has expired + none + ;; present, and lock has not expired + (some staking-info) + ) + ;; no state at all + none + ) +) + +(define-read-only (get-pool-info (owner principal)) + (map-get? pools owner) +) + +(define-read-only (get-pox-info) + (ok { + min-amount-ustx: MIN_STACKING_AMOUNT, + reward-cycle-id: (current-pox-reward-cycle), + prepare-cycle-length: (var-get pox-prepare-cycle-length), + first-burnchain-block-height: (var-get first-burnchain-block-height), + reward-cycle-length: (var-get pox-reward-cycle-length), + total-liquid-supply-ustx: stx-liquid-supply, + }) +) + +;;; Public functions + +(define-public (stake-pooled + (pool-owner ) + (amount-ustx uint) + (num-cycles uint) + (unlock-bytes (buff 683)) + (start-burn-ht uint) + ) + (let ((owner (contract-of pool-owner))) + (asserts! (is-some (get-pool-info owner)) ERR_POOL_NOT_FOUND) + (try! (contract-call? pool-owner validate-stake! tx-sender amount-ustx + num-cycles unlock-bytes + )) + (inner-stake amount-ustx num-cycles unlock-bytes start-burn-ht (ok owner)) + ) +) + +;; #[allow(unnecessary_public)] +(define-public (stake + (amount-ustx uint) + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + (start-burn-ht uint) + (signer-sig (optional (buff 65))) + (signer-key (buff 33)) + (max-amount uint) + (auth-id uint) + (num-cycles uint) + (unlock-bytes (buff 683)) + ) + ;; this stacker's first reward cycle is the _next_ reward cycle + (begin + ;; pox-addr must be valid + (try! (check-pox-addr pox-addr)) + + (try! (validate-signer-key-usage pox-addr (current-pox-reward-cycle) "stake" + num-cycles signer-sig signer-key amount-ustx max-amount auth-id + tx-sender + )) + + (inner-stake amount-ustx num-cycles unlock-bytes start-burn-ht + (err { + pox-addr: pox-addr, + signer-key: signer-key, + }) + ) + ) +) + +(define-private (inner-stake + (amount-ustx uint) + (num-cycles uint) + (unlock-bytes (buff 683)) + (start-burn-ht uint) + (pool-or-solo-info (response principal { + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + signer-key: (buff 33), + })) + ) + (let ( + (current-cycle (current-pox-reward-cycle)) + (first-reward-cycle (+ u1 current-cycle)) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) + (unlock-cycle (+ current-cycle num-cycles)) + (unlock-burn-height (reward-cycle-to-unlock-height unlock-cycle)) + ) + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their `stack-stx` transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + ERR_INVALID_START_BURN_HEIGHT + ) + + ;; amount must be valid + (asserts! (>= amount-ustx MIN_STACKING_AMOUNT) ERR_INVALID_AMOUNT) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) + + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + ;; (asserts! (check-caller-allowed) (err ERR_STACKING_PERMISSION_DENIED)) + + ;;;; tx-sender principal must not be stacking + (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) + + ;;;; the Stacker must have sufficient unlocked funds + (asserts! (>= (stx-get-balance tx-sender) amount-ustx) + ERR_INSUFFICIENT_FUNDS + ) + + (try! (add-staker-to-reward-cycles tx-sender first-reward-cycle num-cycles)) + + (map-set staking-state tx-sender { + amount-ustx: amount-ustx, + unlock-bytes: unlock-bytes, + first-reward-cycle: first-reward-cycle, + num-cycles: num-cycles, + pool-or-solo-info: pool-or-solo-info, + }) + + (ok { + stacker: tx-sender, + unlock-burn-height: unlock-burn-height, + unlock-bytes: unlock-bytes, + amount-ustx: amount-ustx, + unlock-cycle: unlock-cycle, + num-cycles: num-cycles, + pool-or-solo-info: pool-or-solo-info, + }) + ) +) + +(define-public (stake-extend-pooled + (pool-owner ) + (amount-ustx uint) + (num-cycles uint) + (unlock-bytes (buff 683)) + ) + (let ((owner (contract-of pool-owner))) + (asserts! (is-some (get-pool-info owner)) ERR_POOL_NOT_FOUND) + (try! (contract-call? pool-owner validate-stake! tx-sender amount-ustx + num-cycles unlock-bytes + )) + (inner-stake-extend amount-ustx num-cycles unlock-bytes (ok owner)) + ) +) + +;; #[allow(unnecessary_public)] +(define-public (stake-extend + (amount-ustx uint) + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + ;; #[allow(unused_binding)] + (signer-sig (optional (buff 65))) + (signer-key (buff 33)) + ;; #[allow(unused_binding)] + (max-amount uint) + ;; #[allow(unused_binding)] + (auth-id uint) + (num-cycles uint) + (unlock-bytes (buff 683)) + ) + (begin + (try! (validate-signer-key-usage pox-addr (current-pox-reward-cycle) + "stake-extend" num-cycles signer-sig signer-key amount-ustx + max-amount auth-id tx-sender + )) + + ;; pox-addr must be valid + (try! (check-pox-addr pox-addr)) + + (inner-stake-extend amount-ustx num-cycles unlock-bytes + (err { + pox-addr: pox-addr, + signer-key: signer-key, + }) + ) + ) +) + +(define-private (inner-stake-extend + (amount-ustx uint) + (num-cycles uint) + (unlock-bytes (buff 683)) + (pool-or-solo-info (response principal { + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + signer-key: (buff 33), + })) + ) + (let ( + (current-stacker-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKED)) + (prev-unlock-cycle (- + (+ (get first-reward-cycle current-stacker-info) + (get num-cycles current-stacker-info) + ) + u1 + )) + (current-cycle (current-pox-reward-cycle)) + (unlock-cycle (+ current-cycle num-cycles)) + (unlock-burn-height (reward-cycle-to-unlock-height unlock-cycle)) + (account-info (stx-account tx-sender)) + ) + (asserts! (is-eq prev-unlock-cycle current-cycle) ERR_CANNOT_EXTEND) + + (try! (add-staker-to-reward-cycles tx-sender (+ current-cycle u1) num-cycles)) + + ;; The caller has locked STX - we need to ensure that their locked + unlocked balance + ;; is sufficient + (asserts! + (>= (+ (get locked account-info) (get unlocked account-info)) + amount-ustx + ) + ERR_INSUFFICIENT_FUNDS + ) + + ;; amount must be valid + (asserts! (> amount-ustx u0) ERR_INVALID_AMOUNT) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) + + (map-set staking-state tx-sender { + amount-ustx: amount-ustx, + first-reward-cycle: (+ current-cycle u1), + num-cycles: num-cycles, + unlock-bytes: unlock-bytes, + pool-or-solo-info: pool-or-solo-info, + }) + + (ok { + stacker: tx-sender, + unlock-burn-height: unlock-burn-height, + unlock-bytes: unlock-bytes, + amount-ustx: amount-ustx, + unlock-cycle: unlock-cycle, + num-cycles: num-cycles, + pool-or-solo-info: pool-or-solo-info, + }) + ) +) + +(define-public (register-pool + (pool-owner ) + (signer-key (buff 33)) + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + ;; #[allow(unused_binding)] + (signer-sig (buff 65)) + ;; #[allow(unused_binding)] + (auth-id uint) + ) + (let ((owner (contract-of pool-owner))) + (try! (verify-signer-key-grant tx-sender signer-key pox-addr)) + + (try! (check-pox-addr pox-addr)) + + (try! (contract-call? pool-owner validate-management! tx-sender signer-key + pox-addr + )) + + (map-set pools owner { + signer-key: signer-key, + pox-addr: pox-addr, + }) + (ok { + owner: owner, + signer-key: signer-key, + pox-addr: pox-addr, + }) + ) +) + +;; Allow a user to update their staked STX amount or pool while they are staked. +(define-public (stake-update-pooled + (pool-owner ) + (amount-ustx-increase uint) + ) + (let ( + (owner (contract-of pool-owner)) + (staker-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKED)) + ) + (asserts! (is-some (get-pool-info owner)) ERR_POOL_NOT_FOUND) + (try! (contract-call? pool-owner validate-stake! tx-sender + (+ (get amount-ustx staker-info) amount-ustx-increase) + (get num-cycles staker-info) (get unlock-bytes staker-info) + )) + (inner-stake-update amount-ustx-increase (ok owner)) + ) +) + +;; Allow a user to update their staked STX amount, signer key, +;; and/or PoX address while they are staked. +;; +;; #[allow(unnecessary_public)] +(define-public (stake-update + (amount-ustx-increase uint) + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + (signer-key (buff 33)) + ;; #[allow(unused_binding)] + (signer-sig (optional (buff 65))) + ;; #[allow(unused_binding)] + (max-amount uint) + ;; #[allow(unused_binding)] + (auth-id uint) + ) + (begin + ;; pox-addr must be valid + (try! (check-pox-addr pox-addr)) + + (let ( + (stake-update-result (try! (inner-stake-update amount-ustx-increase + (err { + pox-addr: pox-addr, + signer-key: signer-key, + }) + ))) + (cycles-remaining (- (get unlock-cycle stake-update-result) + (current-pox-reward-cycle) + )) + ) + (try! (validate-signer-key-usage pox-addr (current-pox-reward-cycle) + "stake-update" cycles-remaining signer-sig signer-key + amount-ustx-increase max-amount auth-id tx-sender + )) + (ok stake-update-result) + ) + ) +) + +(define-private (inner-stake-update + (amount-ustx-increase uint) + (pool-or-solo-info (response principal { + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + signer-key: (buff 33), + })) + ) + (let ( + (current-stacker-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKED)) + (new-amount-ustx (+ (get amount-ustx current-stacker-info) amount-ustx-increase)) + (unlock-cycle (- + (+ (get first-reward-cycle current-stacker-info) + (get num-cycles current-stacker-info) + ) + u1 + )) + ) + ;; assert that the amount of STX to increase is greater than 0 + (asserts! (> amount-ustx-increase u0) ERR_INVALID_AMOUNT) + + ;; assert that the staker has sufficient STX to increase their stake + (asserts! (>= (stx-get-balance tx-sender) amount-ustx-increase) + ERR_INSUFFICIENT_FUNDS + ) + + (map-set staking-state tx-sender { + amount-ustx: new-amount-ustx, + first-reward-cycle: (get first-reward-cycle current-stacker-info), + num-cycles: (get num-cycles current-stacker-info), + unlock-bytes: (get unlock-bytes current-stacker-info), + pool-or-solo-info: pool-or-solo-info, + }) + + (ok { + stacker: tx-sender, + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + unlock-bytes: (get unlock-bytes current-stacker-info), + amount-ustx: new-amount-ustx, + unlock-cycle: unlock-cycle, + num-cycles: (get num-cycles current-stacker-info), + pool-or-solo-info: pool-or-solo-info, + }) + ) +) + +;;; Signer key authorization functions + +(define-public (grant-signer-key + (signer-key (buff 33)) + (staker principal) + (pox-addr (optional { + version: (buff 1), + hashbytes: (buff 32), + })) + (auth-id uint) + (signer-sig (buff 65)) + ) + (begin + (asserts! + (is-none (map-get? used-signer-key-grants { + signer-key: signer-key, + staker: staker, + auth-id: auth-id, + })) + ERR_SIGNER_KEY_GRANT_USED + ) + + (asserts! + (is-eq + (unwrap! + (secp256k1-recover? + (get-signer-grant-message-hash staker pox-addr auth-id) + signer-sig + ) + ERR_INVALID_SIGNATURE_RECOVER + ) + signer-key + ) + ERR_INVALID_SIGNATURE_PUBKEY + ) + + (asserts! + (map-insert used-signer-key-grants { + signer-key: signer-key, + staker: staker, + auth-id: auth-id, + } + true + ) + ERR_SIGNER_KEY_GRANT_USED + ) + + (map-set signer-key-grants { + signer-key: signer-key, + staker: staker, + } + pox-addr + ) + + (ok true) + ) +) + +;; Revoke a signer key grant for a staker. Only the Stacks principal +;; associated with `signer-key` can call this function. +;; +;; Returns a boolean indicating whether the signer key grant existed. +(define-public (revoke-signer-grant + (staker principal) + (signer-key (buff 33)) + ) + (begin + ;; Validate that `tx-sender` has the same pubkey hash as `signer-key` + (asserts! + (is-eq + (unwrap-panic (principal-construct? + (if is-in-mainnet + STACKS_ADDR_VERSION_MAINNET + STACKS_ADDR_VERSION_TESTNET + ) + (hash160 signer-key) + )) + tx-sender + ) + ERR_NOT_ALLOWED + ) + (ok (map-delete signer-key-grants { + signer-key: signer-key, + staker: staker, + })) + ) +) + +;; Generate a message hash for validating a signer key. +;; The message hash follows SIP018 for signing structured data. The structured data +;; is the tuple `{ pox-addr: { version, hashbytes }, reward-cycle, auth-id, max-amount, topic, period }`. +;; The domain is [POX_5_SIGNER_DOMAIN]. +(define-read-only (get-signer-key-message-hash + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + (reward-cycle uint) + (topic (string-ascii 14)) + (period uint) + (max-amount uint) + (auth-id uint) + ) + (sha256 (concat SIP018_MSG_PREFIX + (concat (sha256 (unwrap-panic (to-consensus-buff? POX_5_SIGNER_DOMAIN))) + (sha256 (unwrap-panic (to-consensus-buff? { + pox-addr: pox-addr, + reward-cycle: reward-cycle, + topic: topic, + period: period, + auth-id: auth-id, + max-amount: max-amount, + }))) + ))) +) + +;; Construct the message hash for validating a signer key grant. Unlike [get-signer-key-message-hash], +;; this message hash does not include `max-amount`, `period`, or `reward-cycle`. The topic is always `"grant-authorization"`. +;; The `pox-addr` field is optional. When `none`, it means the signer key can be used for any PoX address. +(define-read-only (get-signer-grant-message-hash + (staker principal) + (pox-addr (optional { + version: (buff 1), + hashbytes: (buff 32), + })) + (auth-id uint) + ) + (sha256 (concat SIP018_MSG_PREFIX + (concat (sha256 (unwrap-panic (to-consensus-buff? POX_5_SIGNER_DOMAIN))) + (sha256 (unwrap-panic (to-consensus-buff? { + topic: "grant-authorization", + staker: staker, + pox-addr: pox-addr, + auth-id: auth-id, + }))) + ))) +) + +;; Verify a signature from the signing key for this specific stacker. +;; See `get-signer-key-message-hash` for details on the message hash. +;; +;; Note that `reward-cycle` corresponds to the _current_ reward cycle, +;; when used with `stack-stx` and `stack-extend`. Both the reward cycle and +;; the lock period are inflexible, which means that the stacker must confirm their transaction +;; during the exact reward cycle and with the exact period that the signature or authorization was +;; generated for. +;; +;; The `amount` field is checked to ensure it is not larger than `max-amount`, which is +;; a field in the authorization. `auth-id` is a random uint to prevent authorization +;; replays. +;; +;; This function does not verify the payload of the authorization. The caller of +;; this function must ensure that the payload (reward cycle, period, topic, and pox-addr) +;; are valid according to the caller function's requirements. +;; +;; When `signer-sig` is present, the public key is recovered from the signature +;; and compared to `signer-key`. If `signer-sig` is `none`, the function verifies that an authorization was previously +;; added for this key. +;; +;; This function checks to ensure that the authorization hasn't been used yet, but it +;; does _not_ store the authorization as used. The function `consume-signer-key-authorization` +;; handles that, and this read-only function is exposed for client-side verification. +(define-read-only (verify-signer-key-sig + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + (reward-cycle uint) + (topic (string-ascii 14)) + (period uint) + (signer-sig (buff 65)) + (signer-key (buff 33)) + (amount uint) + (max-amount uint) + (auth-id uint) + ) + (begin + ;; Validate that amount is less than or equal to `max-amount` + (asserts! (>= max-amount amount) ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH) + (asserts! + (is-none (map-get? used-signer-key-authorizations { + signer-key: signer-key, + reward-cycle: reward-cycle, + topic: topic, + period: period, + pox-addr: pox-addr, + auth-id: auth-id, + max-amount: max-amount, + })) + ERR_SIGNER_AUTH_USED + ) + (ok (asserts! + (is-eq + (unwrap! + (secp256k1-recover? + (get-signer-key-message-hash pox-addr reward-cycle topic + period max-amount auth-id + ) + signer-sig + ) + ERR_INVALID_SIGNATURE_RECOVER + ) + signer-key + ) + ERR_INVALID_SIGNATURE_PUBKEY + )) + ) +) + +;; This function does two things: +;; +;; - Verify that a signer key is authorized to be used +;; - Updates the `used-signer-key-authorizations` map to prevent reuse +;; +;; This "wrapper" method around `verify-signer-key-sig` allows that function to remain +;; read-only, so that it can be used by clients as a sanity check before submitting a transaction. +(define-private (consume-signer-key-authorization + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + (reward-cycle uint) + (topic (string-ascii 14)) + (period uint) + (signer-sig (buff 65)) + (signer-key (buff 33)) + (amount uint) + (max-amount uint) + (auth-id uint) + ) + (begin + ;; verify the authorization + (try! (verify-signer-key-sig pox-addr reward-cycle topic period signer-sig + signer-key amount max-amount auth-id + )) + ;; update the `used-signer-key-authorizations` map + (asserts! + (map-insert used-signer-key-authorizations { + signer-key: signer-key, + reward-cycle: reward-cycle, + topic: topic, + period: period, + pox-addr: pox-addr, + auth-id: auth-id, + max-amount: max-amount, + } + true + ) + ERR_SIGNER_AUTH_USED + ) + (ok true) + ) +) + +;; if signer-sig-opt is present, verify the signature. Otherwise, +;; verify that a grant was previously added for this key. +(define-private (validate-signer-key-usage + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + (reward-cycle uint) + (topic (string-ascii 14)) + (period uint) + (signer-sig-opt (optional (buff 65))) + (signer-key (buff 33)) + (amount uint) + (max-amount uint) + (auth-id uint) + (staker principal) + ) + (match signer-sig-opt + signer-sig (consume-signer-key-authorization pox-addr reward-cycle topic period + signer-sig signer-key amount max-amount auth-id + ) + (verify-signer-key-grant staker signer-key pox-addr) + ) +) + +(define-read-only (verify-signer-key-grant + (staker principal) + (signer-key (buff 33)) + (pox-addr { + version: (buff 1), + hashbytes: (buff 32), + }) + ) + (ok (asserts! + (match (unwrap! + (map-get? signer-key-grants { + signer-key: signer-key, + staker: staker, + }) + ERR_SIGNER_KEY_GRANT_NOT_FOUND + ) + grant-pox-addr (is-eq grant-pox-addr pox-addr) + true + ) + ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH + )) +) + +;;; Validation helpers + +(define-read-only (check-pox-lock-period (lock-period uint)) + (and + (>= lock-period u1) + (<= lock-period MAX_NUM_CYCLES) + ) +) + +(define-read-only (check-pox-addr (pox-addr { + version: (buff 1), + hashbytes: (buff 32), +})) + (let ( + (version (buff-to-uint-be (get version pox-addr))) + (expected-len (if (<= version MAX_ADDRESS_VERSION_BUFF_20) + u20 + u32 + )) + ) + (ok (asserts! + (and + (<= version MAX_ADDRESS_VERSION) + (is-eq (len (get hashbytes pox-addr)) expected-len) + ) + ERR_INVALID_POX_ADDRESS + )) + ) +) + +;; Is the address mode valid for a PoX address? +(define-read-only (check-pox-addr-version (version (buff 1))) + (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION) +) + +;; Is this buffer the right length for the given PoX address? +(define-read-only (check-pox-addr-hashbytes + (version (buff 1)) + (hashbytes (buff 32)) + ) + (if (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION_BUFF_20) + (is-eq (len hashbytes) u20) + (if (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION_BUFF_32) + (is-eq (len hashbytes) u32) + false + ) + ) +) + +;;; Cycle-based Linked List functions + +;; First item in the linked list of stakers +(define-map staker-set-ll-first-for-cycle + uint + principal +) +;; Last item in the linked list of stakers +(define-map staker-set-ll-last-for-cycle + uint + principal +) + +;; Linked list of all stakers for a cycle +(define-map staker-set-ll-for-cycle + { + cycle: uint, + staker: principal, + } + { + prev: (optional principal), + next: (optional principal), + } +) + +(define-read-only (get-staker-set-last-item-for-cycle (cycle uint)) + (map-get? staker-set-ll-last-for-cycle cycle) +) + +(define-read-only (get-staker-set-first-item-for-cycle (cycle uint)) + (map-get? staker-set-ll-first-for-cycle cycle) +) + +(define-read-only (get-staker-set-item-for-cycle + (staker principal) + (cycle uint) + ) + (map-get? staker-set-ll-for-cycle { + cycle: cycle, + staker: staker, + }) +) + +(define-read-only (get-staker-set-next-item-for-cycle + (staker principal) + (cycle uint) + ) + (match (map-get? staker-set-ll-for-cycle { + cycle: cycle, + staker: staker, + }) + item (get next item) + none + ) +) + +(define-read-only (get-staker-set-prev-item-for-cycle + (staker principal) + (cycle uint) + ) + (match (map-get? staker-set-ll-for-cycle { + cycle: cycle, + staker: staker, + }) + item (get prev item) + none + ) +) + +(define-read-only (staker-set-contains-for-cycle + (staker principal) + (cycle uint) + ) + (is-some (map-get? staker-set-ll-for-cycle { + cycle: cycle, + staker: staker, + })) +) + +(define-private (add-staker-to-set-for-cycle + (staker principal) + (cycle uint) + ) + (let ((last-item (map-get? staker-set-ll-last-for-cycle cycle))) + ;; Todo: remove this and guard in a higher-level fn + (asserts! + (not (is-some (map-get? staker-set-ll-for-cycle { + cycle: cycle, + staker: staker, + }))) + ERR_ALREADY_STAKED + ) + + (match last-item + last-stacker (let ((last-node (unwrap-panic (map-get? staker-set-ll-for-cycle { + cycle: cycle, + staker: last-stacker, + })))) + (map-set staker-set-ll-for-cycle { + cycle: cycle, + staker: last-stacker, + } { + prev: (get prev last-node), + next: (some staker), + }) + (map-set staker-set-ll-for-cycle { + cycle: cycle, + staker: staker, + } { + prev: (some last-stacker), + next: none, + }) + ) + (begin + ;; This is the first item + (map-set staker-set-ll-for-cycle { + cycle: cycle, + staker: staker, + } { + prev: none, + next: none, + }) + (map-set staker-set-ll-first-for-cycle cycle staker) + ) + ) + + (map-set staker-set-ll-last-for-cycle cycle staker) + (ok true) + ) +) + +(define-private (add-staker-to-reward-cycles + (staker principal) + (first-reward-cycle uint) + (num-cycles uint) + ) + (let ((cycle-indexes (unwrap! + (slice? + (list + u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 + u16 u17 u18 u19 u20 u21 u22 u23 + ) + u0 num-cycles + ) + ERR_INVALID_NUM_CYCLES + ))) + (try! (fold add-staker-to-nth-reward-cycle cycle-indexes + (ok { + staker: staker, + first-reward-cycle: first-reward-cycle, + }) + )) + (ok true) + ) +) + +(define-private (add-staker-to-nth-reward-cycle + (cycle-index uint) + (params-resp (response { + staker: principal, + first-reward-cycle: uint, + } + uint + )) + ) + (let ((params (try! params-resp))) + (try! (add-staker-to-set-for-cycle (get staker params) + (+ (get first-reward-cycle params) cycle-index) + )) + (ok params) + ) +) diff --git a/stacking/deployments/default.simnet-plan.yaml b/stacking/deployments/default.simnet-plan.yaml new file mode 100644 index 0000000..6dcc215 --- /dev/null +++ b/stacking/deployments/default.simnet-plan.yaml @@ -0,0 +1,74 @@ +id: 0 +name: Simulated deployment, used as a default for `clarinet console`, `clarinet test` and `clarinet check` +network: simnet +genesis: + wallets: + - name: deployer + address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_1 + address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_10 + address: ST3FFKYTTB975A3JC3F99MM7TXZJ406R3GKE6JV56 + balance: '200000000000000' + sbtc-balance: '1000000000' + - name: wallet_2 + address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_3 + address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_4 + address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_5 + address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_6 + address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_7 + address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_8 + address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP + balance: '100000000000000' + sbtc-balance: '1000000000' + - name: wallet_9 + address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 + balance: '100000000000000' + sbtc-balance: '1000000000' + contracts: + - genesis + - lockup + - bns + - cost-voting + - costs + - pox + - costs-2 + - pox-2 + - costs-3 + - pox-3 + - pox-4 + - signers + - signers-voting + - costs-4 +plan: + batches: + - id: 0 + transactions: + - transaction-type: emulated-contract-publish + contract-name: pox-5 + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/pox-5.clar + clarity-version: 4 + epoch: '3.3' diff --git a/stacking/flood.ts b/stacking/flood.ts index c2fc5aa..852bcd6 100644 --- a/stacking/flood.ts +++ b/stacking/flood.ts @@ -1,19 +1,15 @@ -import { StacksTestnet } from '@stacks/network'; import { StackingClient } from '@stacks/stacking'; import { - TransactionVersion, getAddressFromPrivateKey, - getNonce, - makeSTXTokenTransfer, broadcastTransaction, makeRandomPrivKey, - StacksTransaction, makeContractDeploy, makeContractCall, - tupleCV, uintCV, AnchorMode, PostConditionMode, + StacksTransactionWire, + fetchNonce, } from '@stacks/transactions'; import { readFileSync } from 'fs'; import { config } from 'dotenv'; @@ -22,20 +18,21 @@ if (process.argv.slice(2).length > 0) { config({ path: './tx-broadcaster.env' }); } import { bytesToHex } from '@stacks/common'; -import { logger, parseEnvInt, contractsApi, accountsApi } from './common'; +import { logger, parseEnvInt, contractsApi, accountsApi, network } from './common.js'; const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2'); -const url = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`; -const network = new StacksTestnet({ url }); const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0'); const bootstrapperKey = process.env.BOOTSTRAPPER_KEY!; const bootstrapper = { privKey: bootstrapperKey, - stxAddress: getAddressFromPrivateKey(bootstrapperKey, TransactionVersion.Testnet), + stxAddress: getAddressFromPrivateKey(bootstrapperKey, network), }; -const client = new StackingClient(bootstrapper.stxAddress, network); +const client = new StackingClient({ + address: bootstrapper.stxAddress, + network, +}); const floodContract = readFileSync('./flooder.clar', { encoding: 'utf-8' }); @@ -48,8 +45,8 @@ const flooders: { privKey: string; stxAddress: string; nonce: bigint }[] = []; for (let i = 0; i < NUM_FLOODERS; i++) { const privKey = makeRandomPrivKey(); flooders.push({ - privKey: bytesToHex(privKey.data), - stxAddress: getAddressFromPrivateKey(privKey.data, TransactionVersion.Testnet), + privKey, + stxAddress: getAddressFromPrivateKey(privKey, network), nonce: BigInt(0), }); } @@ -58,7 +55,7 @@ let hasSentToFlooders = false; const floodContractDeployer = bootstrapper.stxAddress; async function bootstrapFlooders() { - const nonce = await getNonce(bootstrapper.stxAddress, network); + const nonce = await fetchNonce({ address: bootstrapper.stxAddress, network }); logger.info('Bootstrapping flooders'); // sync iterate let i = 0n; @@ -80,7 +77,6 @@ async function bootstrapFlooders() { network, nonce: nonce + i, senderKey: bootstrapper.privKey, - anchorMode: AnchorMode.Any, codeBody: contractBody, }); await broadcast(bootstrapTx, bootstrapper.stxAddress); @@ -94,7 +90,6 @@ async function bootstrapFlooders() { contractName: 'flood', codeBody: floodContract, fee: 3000000, - anchorMode: 'any', network, postConditionMode: PostConditionMode.Allow, }), @@ -145,7 +140,6 @@ async function flood() { functionArgs: [uintCV(1), uintCV(2), uintCV(3)], senderKey: flooder.privKey, nonce: nonce + i, - anchorMode: 'any', network, fee: 10000, }); @@ -156,11 +150,14 @@ async function flood() { await Promise.all(accountFloods); } -async function broadcast(tx: StacksTransaction, sender?: string) { +async function broadcast(tx: StacksTransactionWire, sender?: string) { const txType = tx.payload.payloadType; const label = sender ? accountLabel(sender) : 'Unknown'; - const broadcastResult = await broadcastTransaction(tx, network); - if (broadcastResult.error) { + const broadcastResult = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('error' in broadcastResult) { logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`); return false; } else { @@ -185,7 +182,7 @@ async function waitForNakamoto() { break; } } catch (error) { - if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) { + if (error instanceof Error && 'cause' in error && error.cause instanceof Error && /(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause.message)) { logger.info(`Stacks node not ready, waiting...`); } else { logger.error('Error getting pox info:', error); diff --git a/stacking/monitor.ts b/stacking/monitor.ts index 040fc45..0973b11 100644 --- a/stacking/monitor.ts +++ b/stacking/monitor.ts @@ -1,4 +1,4 @@ -import { bitcoinRPC } from './btc-rpc'; +import { bitcoinRPC } from './btc-rpc.js'; import { accounts, nodeUrl, @@ -10,7 +10,7 @@ import { txApi, logger, WALLET_NAME, -} from './common'; +} from './common.js'; import { Transaction, ContractCallTransaction } from '@stacks/stacks-blockchain-api-types'; let lastBurnHeight = 0; @@ -42,13 +42,21 @@ async function getBtcStakerBalance() { return balance; } +async function getLatestBlock() { + try { + return await blocksApi.getBlock({ + heightOrHash: 'latest', + }); + } catch (error) { + return null; + } +} + async function getInfo() { - let { client } = accounts[0]; + let { client } = accounts[0]!; const [poxInfo, blockInfo, txs, btcStakerBalance] = await Promise.all([ client.getPoxInfo(), - blocksApi.getBlock({ - heightOrHash: 'latest', - }), + getLatestBlock(), getTransactions(), getBtcStakerBalance(), ]); @@ -88,22 +96,22 @@ async function loop() { try { const { poxInfo, blockInfo, ...info } = await getInfo(); let { reward_cycle_id, current_burnchain_block_height } = poxInfo; - let { height } = blockInfo; + const height = blockInfo?.height ?? 0; let showBurnMsg = false; let showPrepareMsg = false; let showCycleMsg = false; let showStxBlockMsg = false; - let burnHeightDate = new Date(blockInfo.burn_block_time * 1000); + let burnHeightDate = new Date(blockInfo?.burn_block_time ?? 0 * 1000); let burnHeightTimeAgo = (new Date().getTime() - burnHeightDate.getTime()) / 1000; const loopLog = logger.child({ height, burnHeight: current_burnchain_block_height, // burnHeightTime: cycle: reward_cycle_id, - txCount: blockInfo.tx_count, + txCount: blockInfo?.tx_count, rewardCycle: reward_cycle_id, lastBurnBlock: `${burnHeightTimeAgo.toFixed(0)}s ago`, - burnHash: blockInfo.burn_block_hash, + burnHash: blockInfo?.burn_block_hash, btcStakerBalance: info.btcStakerBalance, }); @@ -155,6 +163,7 @@ async function loop() { if (current_burnchain_block_height === EPOCH_30_START) { loopLog.info('Starting Nakamoto'); } + // loopLog.info({ poxInfo }); } if (showPrepareMsg) { loopLog.info( @@ -176,7 +185,7 @@ async function loop() { } } - if (!showBurnMsg && showStxBlockMsg && blockInfo.burn_block_height >= EPOCH_30_START) { + if (!showBurnMsg && showStxBlockMsg && (blockInfo?.burn_block_height ?? 0) >= EPOCH_30_START) { loopLog.info({ lastStxBlockDiff: lastStxBlockDiff / 1000 }, 'Nakamoto block'); } if (showStxBlockMsg && info.txs.length > 0) { diff --git a/stacking/package.json b/stacking/package.json index 00939cc..12db4a0 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -11,28 +11,32 @@ "author": "", "license": "ISC", "dependencies": { + "@clarigen/core": "^4.1.3", "@noble/curves": "^2.0.1", "@scure/base": "^1.2.0", "@scure/btc-signer": "^1.5.0", "@stacks/api": "6.11.4-pr.472091f.0", "@stacks/blockchain-api-client": "7.8.2", - "@stacks/common": "6.11.4-pr.36558cf.0", - "@stacks/encryption": "6.11.4-pr.36558cf.0", - "@stacks/network": "6.11.4-pr.36558cf.0", - "@stacks/stacking": "6.11.4-pr.36558cf.0", + "@stacks/common": "7.3.1", + "@stacks/encryption": "7.4.0", + "@stacks/network": "7.3.1", + "@stacks/stacking": "7.4.0", "@stacks/stacks-blockchain-api-types": "7.8.2", - "@stacks/transactions": "6.11.4-pr.36558cf.0", + "@stacks/transactions": "7.4.0", "dotenv": "^16.4.5", "pino": "^8.19.0", "pino-pretty": "^10.3.1" }, "devDependencies": { + "@clarigen/cli": "^4.1.3", "@dotenvx/dotenvx": "^0.26.0", "@stacks/prettier-config": "^0.0.10", - "tsx": "4.7.1" + "@total-typescript/tsconfig": "^1.0.4", + "tsx": "4.7.1", + "typescript": "^6.0.2" }, "prettier": "@stacks/prettier-config", "resolutions": { - "@stacks/network": "6.11.4-pr.36558cf.0" + "@stacks/network": "7.4.0" } } diff --git a/stacking/settings/Devnet.toml b/stacking/settings/Devnet.toml new file mode 100644 index 0000000..6e75cb2 --- /dev/null +++ b/stacking/settings/Devnet.toml @@ -0,0 +1,79 @@ +[network] +name = "devnet" + +[accounts.deployer] +mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +balance = 100_000_000_000_000 +# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601 +# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM +# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH + +[accounts.wallet_1] +mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild" +balance = 100_000_000_000_000 +# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801 +# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 +# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC + +[accounts.wallet_2] +mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital" +balance = 100_000_000_000_000 +# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101 +# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG +# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG + +[accounts.wallet_3] +mnemonic = "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high" +balance = 100_000_000_000_000 +# secret_key: d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901 +# stx_address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC +# btc_address: mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7 + +[accounts.wallet_4] +mnemonic = "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin" +balance = 100_000_000_000_000 +# secret_key: f9d7206a47f14d2870c163ebab4bf3e70d18f5d14ce1031f3902fbbc894fe4c701 +# stx_address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND +# btc_address: mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8 + +[accounts.wallet_5] +mnemonic = "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase" +balance = 100_000_000_000_000 +# secret_key: 3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801 +# stx_address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB +# btc_address: mweN5WVqadScHdA81aATSdcVr4B6dNokqx + +[accounts.wallet_6] +mnemonic = "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy" +balance = 100_000_000_000_000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt + +[accounts.wallet_7] +mnemonic = "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow" +balance = 100_000_000_000_000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 + +[accounts.wallet_8] +mnemonic = "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune" +balance = 100_000_000_000_000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw + +[accounts.wallet_9] +mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +balance = 100_000_000_000_000 +# secret_key: de433bdfa14ec43aa1098d5be594c8ffb20a31485ff9de2923b2689471c401b801 +# stx_address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 +# btc_address: mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d + +[accounts.wallet_10] +mnemonic = "kiss denial decade slide spawn medal twist lamp evidence economy torch alter witness paper rule snack cushion hill sugar fury public innocent almost divide" +balance = 200_000_000_000_000 +# secret_key: 5b897659452b9f3642be69aee75dc3cc84b2386d55ece1312affdbb80a3b2a7d01 +# stx_address: ST3FFKYTTB975A3JC3F99MM7TXZJ406R3GKE6JV56 +# btc_address: n1qwmgbzf1YeHDW6cTxxEwuqnjbqauvKJ1 diff --git a/stacking/stacking.ts b/stacking/stacking.ts index 36a1f84..e3aca00 100644 --- a/stacking/stacking.ts +++ b/stacking/stacking.ts @@ -8,7 +8,7 @@ import { waitForSetup, logger, burnBlockToRewardCycle, -} from './common'; +} from './common.js'; const randInt = () => crypto.randomInt(0, 0xffffffffffff); const stackingInterval = parseEnvInt('STACKING_INTERVAL', true); @@ -21,7 +21,7 @@ let startTxFee = stackingFee; const getNextTxFee = () => startTxFee++; async function run() { - const poxInfo = await accounts[0].client.getPoxInfo(); + const poxInfo = await accounts[0]!.client.getPoxInfo(); if (!poxInfo.contract_id.endsWith('.pox-4')) { // console.log(`Pox contract is not .pox-4, skipping stacking (contract=${poxInfo.contract_id})`); logger.info( diff --git a/stacking/tsconfig.json b/stacking/tsconfig.json new file mode 100644 index 0000000..c9735b6 --- /dev/null +++ b/stacking/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@total-typescript/tsconfig/tsc/no-dom/library", + "compilerOptions": { + "verbatimModuleSyntax": false, + } +} \ No newline at end of file diff --git a/stacking/tx-broadcaster.ts b/stacking/tx-broadcaster.ts index 7f2bdba..055ea0d 100644 --- a/stacking/tx-broadcaster.ts +++ b/stacking/tx-broadcaster.ts @@ -1,40 +1,42 @@ -import { StacksTestnet } from '@stacks/network'; import { StackingClient } from '@stacks/stacking'; import { - TransactionVersion, getAddressFromPrivateKey, - getNonce, makeSTXTokenTransfer, broadcastTransaction, - StacksTransaction, + StacksTransactionWire, + fetchNonce } from '@stacks/transactions'; -import { logger, CHAIN_ID } from './common'; +import { logger, network } from './common.js'; const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2'); const url = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`; -const network = new StacksTestnet({ url }); -network.chainId = CHAIN_ID; const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0'); const accounts = process.env.ACCOUNT_KEYS!.split(',').map(privKey => ({ privKey, - stxAddress: getAddressFromPrivateKey(privKey, TransactionVersion.Testnet), + stxAddress: getAddressFromPrivateKey(privKey, network), })); -const client = new StackingClient(accounts[0].stxAddress, network); +const client = new StackingClient({ + address: accounts[0]!.stxAddress, + network, +}); async function run() { const accountNonces = await Promise.all( accounts.map(async account => { - const nonce = await getNonce(account.stxAddress, network); + const nonce = await fetchNonce({ + address: account.stxAddress, + network, + }); return { ...account, nonce }; }) ); // Send from account with lowest nonce accountNonces.sort((a, b) => Number(a.nonce) - Number(b.nonce)); - const sender = accountNonces[0]; - const recipient = accountNonces[1]; + const sender = accountNonces[0]!; + const recipient = accountNonces[1]!; logger.info( `Sending stx-transfer from ${sender.stxAddress} (nonce=${sender.nonce}) to ${recipient.stxAddress}` @@ -47,16 +49,18 @@ async function run() { network, nonce: sender.nonce, fee: 300, - anchorMode: 'any', }); await broadcast(tx, sender.stxAddress); } -async function broadcast(tx: StacksTransaction, sender?: string) { +async function broadcast(tx: StacksTransactionWire, sender?: string) { const txType = tx.payload.payloadType; const label = sender ? accountLabel(sender) : 'Unknown'; - const broadcastResult = await broadcastTransaction(tx, network); - if (broadcastResult.error) { + const broadcastResult = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('error' in broadcastResult) { logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`); return false; } else { @@ -81,7 +85,7 @@ async function waitForNakamoto() { break; } } catch (error) { - if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) { + if (error instanceof Error && 'cause' in error && error.cause instanceof Error && /(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause.message)) { logger.info(`Stacks node not ready, waiting...`); } else { logger.error('Error getting pox info:', error); From e866aeb8ebcd2ece5c1f40341d5a41b563ed4c59 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:23:07 -0700 Subject: [PATCH 05/30] fix: use signer key grants, actually stake --- docker-compose.yml | 4 +- run.sh | 2 +- stacking/btc-staker.ts | 161 +++++++++++++++++--------------------- stacking/common.ts | 42 +++++++--- stacking/contracts.ts | 7 +- stacking/flood.ts | 2 +- stacking/monitor.ts | 31 +++++--- stacking/package.json | 2 +- stacking/pox-5-helpers.ts | 128 ++++++++++++++++++++++++++++++ 9 files changed, 260 insertions(+), 119 deletions(-) create mode 100644 stacking/pox-5-helpers.ts diff --git a/docker-compose.yml b/docker-compose.yml index 5d4e847..8a7b565 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -334,8 +334,8 @@ services: context: . dockerfile: Dockerfile.stacker environment: - STACKS_CORE_RPC_HOST: stacks-node - STACKS_CORE_RPC_PORT: 20443 + STACKS_CORE_RPC_HOST: stacks-api + STACKS_CORE_RPC_PORT: 3999 STACKING_CYCLES: *STACKING_CYCLES STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT diff --git a/run.sh b/run.sh index edaa465..a013ca9 100755 --- a/run.sh +++ b/run.sh @@ -2,4 +2,4 @@ docker compose down --volumes --remove-orphans --timeout=1 --rmi=local # docker compose up --build -docker compose up --build --exit-code-from monitor \ No newline at end of file +docker compose up --build \ No newline at end of file diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 5fce54c..700a54e 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -22,6 +22,7 @@ import { type Account, EPOCH_35_START, WALLET_NAME, + waitForTxConfirmed, } from './common.js'; import { createOrLoadWallet, getNewAddress, listUnspent, sendToAddress } from './btc-rpc.js'; import { @@ -32,6 +33,7 @@ import { } from './btc-locking.js'; import { pox5 } from './contracts.js'; import { TESTNET_BURN_ADDRESS } from '@clarigen/core'; +import { signSignerKeyGrant } from './pox-5-helpers.js'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); const postTxWait = parseEnvInt('POST_TX_WAIT', true); @@ -60,6 +62,34 @@ async function initBtcWallet() { } } +async function submitSignerKeyGrant(account: Account) { + const authId = 1n; + const signature = signSignerKeyGrant({ + staker: account.stxAddress, + poxAddr: null, + authId, + signerSk: hex.decode(account.signerPrivKey), + }); + + const tx = await makeContractCall({ + ...pox5.grantSignerKey({ + signerKey: hex.decode(account.signerPubKey), + staker: account.stxAddress, + poxAddr: null, + authId, + signerSig: signature, + }), + senderKey: account.privKey, + network, + }) + const result = await broadcastTransaction({ + transaction: tx, + network, + }); + account.logger.info({ ...result }, 'L2 signer key grant tx broadcast'); + return result; +} + // -- L2: Stacks contract calls -- async function submitStake( @@ -69,61 +99,29 @@ async function submitStake( ) { const authId = Math.floor(Math.random() * 0xffffffffffff); - const signerSignature = account.client.signPoxSignature({ - topic: Pox4SignatureTopic.StackStx, - rewardCycle: poxInfo.reward_cycle_id, - poxAddress: account.btcAddr, - period: stakingCycles, - signerPrivateKey: account.signerPrivKey, - authId, - maxAmount, - }); - const poxAddr = createAddress(account.stxAddress); - const [contractAddr] = poxInfo.contract_id.split('.'); const stakeFnCall = pox5.stake({ - amountUstx: 100n, + amountUstx: 1000_000000n, poxAddr: { - version: Buffer.from([poxAddr.version]), - hashbytes: Buffer.from(hex.decode(poxAddr.hash160)), + version: new Uint8Array([1]), + hashbytes: hex.decode(poxAddr.hash160), }, startBurnHt: poxInfo.current_burnchain_block_height!, - signerSig: Buffer.from(hex.decode(signerSignature)), - signerKey: Buffer.from(hex.decode(account.signerPubKey)), + signerSig: null, + signerKey: hex.decode(account.signerPubKey), maxAmount, authId, numCycles: stakingCycles, unlockBytes: unlockBytes, }); - const txOptions = { - // ...stakeFnCall, - contractAddress: TESTNET_BURN_ADDRESS, - contractName: 'pox-5', - functionName: 'stake', - functionArgs: [ - // uintCV(account.balance!), - uintCV(100n), - tupleCV({ - version: bufferCV(new Uint8Array([1])), - hashbytes: bufferCV(hex.decode(poxAddr.hash160)), - }), - uintCV(poxInfo.current_burnchain_block_height!), - someCV(bufferCV(hex.decode(signerSignature))), - bufferCV(hex.decode(account.signerPubKey)), - uintCV(maxAmount), - uintCV(authId), - uintCV(stakingCycles), - bufferCV(Buffer.from(unlockBytes)), - ], + const tx = await makeContractCall({ + ...stakeFnCall, senderKey: account.privKey, network, fee: getNextTxFee(), - anchorMode: AnchorMode.Any, - }; - - const tx = await makeContractCall(txOptions); + }); const result = await broadcastTransaction({ transaction: tx, network, @@ -135,36 +133,22 @@ async function submitStake( async function submitStakeExtend(account: Account, poxInfo: any, unlockBytes: Uint8Array) { const authId = Math.floor(Math.random() * 0xffffffffffff); - const signerSignature = account.client.signPoxSignature({ - topic: Pox4SignatureTopic.StackExtend, - rewardCycle: poxInfo.reward_cycle_id, - poxAddress: account.btcAddr, - period: stakingCycles, - signerPrivateKey: account.signerPrivKey, - authId, - maxAmount, - }); - - const [contractAddr, contractName] = poxInfo.contract_id.split('.'); + const poxAddr = createAddress(account.stxAddress); const txOptions = { - contractAddress: contractAddr, - contractName, - functionName: 'stake-extend', - functionArgs: [ - uintCV(stakingCycles), - bufferCV(Buffer.from(unlockBytes)), - tupleCV({ - version: bufferCV(Buffer.from([createAddress(account.stxAddress).version])), - hashbytes: bufferCV( - Buffer.from(hex.decode(createAddress(account.stxAddress).hash160)), - ), - }), - bufferCV(Buffer.from(hex.decode(signerSignature))), - bufferCV(Buffer.from(hex.decode(account.signerPubKey))), - uintCV(maxAmount), - uintCV(authId), - ], + ...pox5.stakeExtend({ + amountUstx: 1000_000000n, + poxAddr: { + version: new Uint8Array([1]), + hashbytes: hex.decode(poxAddr.hash160), + }, + signerSig: null, + signerKey: hex.decode(account.signerPubKey), + maxAmount, + authId, + numCycles: stakingCycles, + unlockBytes, + }), senderKey: account.privKey, network, fee: getNextTxFee(), @@ -201,9 +185,11 @@ async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockB let lastStakedCycle = 0; +let hasGrantedSignerKey = false; + async function run() { const poxInfo = await accounts[0]!.client.getPoxInfo(); - if (poxInfo.current_burnchain_block_height! < EPOCH_35_START) { + if (poxInfo.current_burnchain_block_height! <= EPOCH_35_START) { // logger.info({ burnHeight: poxInfo.current_burnchain_block_height }, 'Not on epoch 3.5 yet, skipping'); return; } @@ -224,34 +210,30 @@ async function run() { const nowCycle = burnBlockToRewardCycle(poxInfo.current_burnchain_block_height ?? 0); + const txIdsToWait: string[] = []; + for (const account of accountInfos) { const unlockBytes = getUnlockBytes(account.pubKey); const unlockBurnHeight = calculateUnlockBurnHeight(currentCycle, stakingCycles, POX_REWARD_LENGTH); - const ALWAYS_STAKE = false; // for now, testing - - if (ALWAYS_STAKE && nowCycle > lastStakedCycle) { - logger.info({ nowCycle, lastStakedCycle }, 'Staking through next cycle'); - // await submitStake(account, poxInfo, unlockBytes); - - await submitBtcLock(account, unlockBurnHeight, unlockBytes); - if (account.lockedAmount === 0n) { - await submitStake(account, poxInfo, unlockBytes); + if (!hasGrantedSignerKey) { + const txResult = await submitSignerKeyGrant(account); + if ('error' in txResult) { + logger.error({ ...txResult }, 'Error granting signer key'); + continue; } - await new Promise(r => setTimeout(r, postTxWait * 1000)); - continue; + await waitForTxConfirmed(txResult.txid); } - // TODO: this won't trigger because we don't have pox-locking for pox-5 yet - if (account.lockedAmount === 0n) { account.logger.info('Account unlocked, staking...', { account: account.index, rewardCycle: poxInfo.reward_cycle_id, }); - await submitStake(account, poxInfo, unlockBytes); - await new Promise(r => setTimeout(r, postTxWait * 1000)); + const stakeResult = await submitStake(account, poxInfo, unlockBytes); + txIdsToWait.push(stakeResult.txid); + // await new Promise(r => setTimeout(r, postTxWait * 1000)); await submitBtcLock(account, unlockBurnHeight, unlockBytes); continue; @@ -259,19 +241,22 @@ async function run() { const unlockCycle = burnBlockToRewardCycle(account.unlockHeight); - if (unlockCycle === nowCycle + 1) { + if (unlockCycle === nowCycle) { account.logger.info({ unlockHeight: account.unlockHeight, nowCycle, unlockCycle }, 'Extending stake...'); - await submitStakeExtend(account, poxInfo, unlockBytes); - await new Promise(r => setTimeout(r, postTxWait * 1000)); + const stakeExtendResult = await submitStakeExtend(account, poxInfo, unlockBytes); + txIdsToWait.push(stakeExtendResult.txid); + // await new Promise(r => setTimeout(r, postTxWait * 1000)); await submitBtcLock(account, unlockBurnHeight, unlockBytes); continue; } - // account.logger.info({ nowCycle, unlockCycle }, 'Staked through next cycle, skipping'); + account.logger.info({ nowCycle, unlockCycle }, 'Staked through next cycle, skipping'); } + await Promise.all(txIdsToWait.map(waitForTxConfirmed)); lastStakedCycle = nowCycle; + hasGrantedSignerKey = true; } async function loop() { diff --git a/stacking/common.ts b/stacking/common.ts index e52f79c..d7be3ac 100644 --- a/stacking/common.ts +++ b/stacking/common.ts @@ -5,13 +5,9 @@ import { } from '@stacks/transactions'; import { getPublicKeyFromPrivate, publicKeyToBtcAddress } from '@stacks/encryption'; import { - InfoApi, - Configuration, - BlocksApi, - TransactionsApi, - SmartContractsApi, - AccountsApi, + createClient, } from '@stacks/blockchain-api-client'; +import { Transaction } from '@stacks/stacks-blockchain-api-types'; import { Logger, pino } from 'pino'; const serviceName = process.env.SERVICE_NAME || 'JS'; @@ -41,14 +37,9 @@ export const nodeUrl = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env export const network = STACKS_TESTNET; network.chainId = CHAIN_ID; network.client.baseUrl = nodeUrl; -const apiConfig = new Configuration({ - basePath: nodeUrl, +export const apiClient = createClient({ + baseUrl: nodeUrl, }); -export const infoApi = new InfoApi(apiConfig); -export const blocksApi = new BlocksApi(apiConfig); -export const txApi = new TransactionsApi(apiConfig); -export const contractsApi = new SmartContractsApi(apiConfig); -export const accountsApi = new AccountsApi(apiConfig); export const EPOCH_30_START = parseEnvInt('STACKS_30_HEIGHT', true); export const EPOCH_25_START = parseEnvInt('STACKS_25_HEIGHT', true); @@ -130,3 +121,28 @@ export function isPreparePhase(burnBlock: number) { export function didCrossPreparePhase(lastBurnHeight: number, newBurnHeight: number) { return isPreparePhase(newBurnHeight) && !isPreparePhase(lastBurnHeight); } + +export async function waitForTxConfirmed(txid: string) { + return new Promise(resolve => { + const interval = setInterval(async () => { + const { data: tx, ...rest } = await apiClient.GET(`/extended/v1/tx/{tx_id}`, { + params: { + path: { + tx_id: txid, + }, + }, + }); + if (!tx) { + logger.warn({ ...rest }, 'Waiting for tx to be confirmed'); + return; + } + if (tx.tx_status !== 'pending') { + if (tx.tx_status !== 'success') { + logger.error({ ...tx }, 'Tx failed'); + } + clearInterval(interval); + resolve(tx); + } + }, 500); + }); +} \ No newline at end of file diff --git a/stacking/contracts.ts b/stacking/contracts.ts index 34de27b..d9f1a60 100644 --- a/stacking/contracts.ts +++ b/stacking/contracts.ts @@ -1,4 +1,7 @@ -import { projectFactory, contractFactory, TESTNET_BURN_ADDRESS } from '@clarigen/core'; -import { project, contracts } from './clarigen-types.js'; +import { contractFactory, TESTNET_BURN_ADDRESS, ClarigenClient } from '@clarigen/core'; +import { contracts } from './clarigen-types.js'; +import { network } from './common.js'; + +export const clarigenClient = new ClarigenClient(network); export const pox5 = contractFactory(contracts.pox5, `${TESTNET_BURN_ADDRESS}.pox-5`); \ No newline at end of file diff --git a/stacking/flood.ts b/stacking/flood.ts index 852bcd6..5c53124 100644 --- a/stacking/flood.ts +++ b/stacking/flood.ts @@ -18,7 +18,7 @@ if (process.argv.slice(2).length > 0) { config({ path: './tx-broadcaster.env' }); } import { bytesToHex } from '@stacks/common'; -import { logger, parseEnvInt, contractsApi, accountsApi, network } from './common.js'; +import { logger, parseEnvInt, network } from './common.js'; const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2'); const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0'); diff --git a/stacking/monitor.ts b/stacking/monitor.ts index 0973b11..73deb2c 100644 --- a/stacking/monitor.ts +++ b/stacking/monitor.ts @@ -5,13 +5,11 @@ import { waitForSetup, EPOCH_30_START, didCrossPreparePhase, - blocksApi, parseEnvInt, - txApi, logger, WALLET_NAME, + apiClient, } from './common.js'; -import { Transaction, ContractCallTransaction } from '@stacks/stacks-blockchain-api-types'; let lastBurnHeight = 0; let lastStxHeight = 0; @@ -27,14 +25,20 @@ const monitorInterval = parseEnvInt('MONITOR_INTERVAL') ?? 2; logger.debug('Exit from monitor?', EXIT_FROM_MONITOR); -async function getTransactions(): Promise { - let res = await txApi.getTransactionsByBlock({ - heightOrHash: 'latest', +async function getTransactions() { + const { data } = await apiClient.GET('/extended/v2/blocks/{height_or_hash}/transactions', { + params: { + path: { + height_or_hash: 'latest', + }, + }, }); - let txs = res.results as Transaction[]; - return txs.filter(tx => { + if (!data) { + return []; + } + return data.results.filter(tx => { return tx.tx_type === 'contract_call'; - }) as ContractCallTransaction[]; + }); } async function getBtcStakerBalance() { @@ -44,9 +48,14 @@ async function getBtcStakerBalance() { async function getLatestBlock() { try { - return await blocksApi.getBlock({ - heightOrHash: 'latest', + const { data } = await apiClient.GET('/extended/v2/blocks/{height_or_hash}', { + params: { + path: { + height_or_hash: 'latest', + }, + }, }); + return data; } catch (error) { return null; } diff --git a/stacking/package.json b/stacking/package.json index 12db4a0..6db960a 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -16,7 +16,7 @@ "@scure/base": "^1.2.0", "@scure/btc-signer": "^1.5.0", "@stacks/api": "6.11.4-pr.472091f.0", - "@stacks/blockchain-api-client": "7.8.2", + "@stacks/blockchain-api-client": "8.15.1", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", diff --git a/stacking/pox-5-helpers.ts b/stacking/pox-5-helpers.ts new file mode 100644 index 0000000..2045553 --- /dev/null +++ b/stacking/pox-5-helpers.ts @@ -0,0 +1,128 @@ +import * as BTC from '@scure/btc-signer'; +import { + Cl, + createAddress, + encodeStructuredDataBytes, + getAddressFromPublicKey, + signWithKey, +} from '@stacks/transactions'; +import { hex } from '@scure/base'; +import { projectErrors, projectFactory } from '@clarigen/core'; +import { accounts, project } from './clarigen-types.js'; +import { sha256 } from '@noble/hashes/sha2.js'; +import { secp256k1 } from '@noble/curves/secp256k1.js'; +import { pox5 } from './contracts.js'; + +export const errorCodes = projectErrors(project).pox5; + +export function toWitnessOutput(script: Uint8Array) { + return BTC.OutScript.encode( + BTC.p2wsh({ + type: 'wsh', + script, + }), + ); +} + +export function serializeLockupScript({ + stacker, + unlockBurnHeight, + unlockBytes, +}: { + stacker: string; + unlockBurnHeight: bigint; + unlockBytes: Uint8Array; +}) { + const addr = createAddress(stacker); + return BTC.Script.encode([ + new Uint8Array([5, addr.version, ...hex.decode(addr.hash160)]), + 'DROP', + Number(unlockBurnHeight), + 'CHECKLOCKTIMEVERIFY', + 'DROP', + unlockBytes, + ]); +} + +export function signSignerKeyGrant({ + staker, + poxAddr, + authId, + signerSk, +}: { + staker: string; + poxAddr: { version: Uint8Array; hashbytes: Uint8Array } | null; + authId: bigint; + signerSk: Uint8Array; +}) { + const message = Cl.tuple({ + staker: Cl.principal(staker), + topic: Cl.stringAscii('grant-authorization'), + 'pox-addr': poxAddr + ? Cl.some( + Cl.tuple({ + version: Cl.buffer(poxAddr.version), + hashbytes: Cl.buffer(poxAddr.hashbytes), + }), + ) + : Cl.none(), + 'auth-id': Cl.uint(authId), + }); + const fullMessage = encodeStructuredDataBytes({ + message, + domain: Cl.tuple({ + name: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.name), + version: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.version), + 'chain-id': Cl.uint(pox5.constants.pOX_5_SIGNER_DOMAIN.chainId), + }), + }); + const data = signWithKey(signerSk, hex.encode(sha256(fullMessage))); + const signature = hex.decode(data.slice(2) + data.slice(0, 2)); + return signature; +} + +/** Get the testnet STX address for a signer key. */ +export function signerAddress(signerKey: Uint8Array) { + return getAddressFromPublicKey(signerKey, 'testnet'); +} + +/** Sign a per-transaction signer authorization (the signer-sig path). */ +export function signPerTransactionAuth({ + signerSk, + poxAddr, + rewardCycle, + topic, + period, + maxAmount, + authId, +}: { + signerSk: Uint8Array; + poxAddr: { version: Uint8Array; hashbytes: Uint8Array }; + rewardCycle: bigint; + topic: string; + period: bigint | number; + maxAmount: bigint | number; + authId: bigint | number; +}) { + const message = Cl.tuple({ + 'pox-addr': Cl.tuple({ + version: Cl.buffer(poxAddr.version), + hashbytes: Cl.buffer(poxAddr.hashbytes), + }), + 'reward-cycle': Cl.uint(rewardCycle), + topic: Cl.stringAscii(topic), + period: Cl.uint(period), + 'auth-id': Cl.uint(authId), + 'max-amount': Cl.uint(maxAmount), + }); + const fullMessage = encodeStructuredDataBytes({ + message, + domain: Cl.tuple({ + name: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.name), + version: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.version), + 'chain-id': Cl.uint(pox5.constants.pOX_5_SIGNER_DOMAIN.chainId), + }), + }); + const data = signWithKey(signerSk, hex.encode(sha256(fullMessage))); + return hex.decode(data.slice(2) + data.slice(0, 2)); +} From 2829901b3fc5de3b5468752778f5adf72e49cfd1 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:19:38 -0700 Subject: [PATCH 06/30] fix: match logic for timelock script --- docker-compose.yml | 6 +++--- stacking/btc-locking.ts | 6 +++--- stacking/btc-staker.ts | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8a7b565..b14f954 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 59d0928bba3f401552ef65b6c0b79ac6dc4cd303 # feat/epoch-3-5-rc + - &STACKS_BLOCKCHAIN_COMMIT 190a195f690025a07371cb2a5e84b3f17cc63c92 # feat/epoch-3-5-rc - &STACKS_API_COMMIT cb5881749553a34704cab62a9631341cb30f2c17 # 8.13.4 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za @@ -9,8 +9,8 @@ x-common-vars: - &BITCOIN_RPC_PASS btc - &MINE_INTERVAL ${MINE_INTERVAL:-1s} - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-2s} # 5 second bitcoin block times in epoch 2.5 - - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-3s} # 30 second bitcoin block times in epoch 3 - - &MINE_INTERVAL_EPOCH35 ${MINE_INTERVAL_EPOCH35:-20s} # 5 second bitcoin block times in epoch 3.5 + - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-2s} # 30 second bitcoin block times in epoch 3 + - &MINE_INTERVAL_EPOCH35 ${MINE_INTERVAL_EPOCH35:-2s} # 5 second bitcoin block times in epoch 3.5 - &NAKAMOTO_BLOCK_INTERVAL 2 # seconds to wait between issuing stx-transfer transactions (which triggers Nakamoto block production) - &STACKS_20_HEIGHT ${STACKS_20_HEIGHT:-0} - &STACKS_2_05_HEIGHT ${STACKS_2_05_HEIGHT:-102} diff --git a/stacking/btc-locking.ts b/stacking/btc-locking.ts index e69c404..f17c18e 100644 --- a/stacking/btc-locking.ts +++ b/stacking/btc-locking.ts @@ -47,9 +47,9 @@ export function calculateUnlockBurnHeight( rewardCycleLength: number, ): bigint { const startCycle = currentCycle + 1; - const lastCycle = startCycle + numCycles - 1; - const lastCycleStartHeight = (lastCycle - 1) * rewardCycleLength; - return BigInt(lastCycleStartHeight + Math.floor(rewardCycleLength / 2)); + const lastCycle = startCycle + numCycles; + const lastCycleStartHeight = (lastCycle * rewardCycleLength) + 1; + return BigInt(lastCycleStartHeight) + (BigInt(rewardCycleLength) / 2n); } // -- P2WSH address from lock script -- diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 700a54e..a08935f 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -229,6 +229,7 @@ async function run() { account.logger.info('Account unlocked, staking...', { account: account.index, rewardCycle: poxInfo.reward_cycle_id, + unlockBurnHeight: unlockBurnHeight.toString(), }); const stakeResult = await submitStake(account, poxInfo, unlockBytes); From 5a7cbd9ec471ec6cd7ef8901b639a44f1dff3e35 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:19:47 -0700 Subject: [PATCH 07/30] fix: ts errors in flood script --- stacking/flood.ts | 24 +++++++++++++++--------- stacking/package.json | 3 ++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/stacking/flood.ts b/stacking/flood.ts index 5c53124..0b1bf8c 100644 --- a/stacking/flood.ts +++ b/stacking/flood.ts @@ -18,7 +18,7 @@ if (process.argv.slice(2).length > 0) { config({ path: './tx-broadcaster.env' }); } import { bytesToHex } from '@stacks/common'; -import { logger, parseEnvInt, network } from './common.js'; +import { logger, parseEnvInt, network, apiClient, nodeUrl } from './common.js'; const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2'); const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0'); @@ -104,10 +104,9 @@ async function bootstrapFlooders() { async function isContractDeployed(address: string) { try { - const result = await contractsApi.getContractSource({ - contractAddress: address, - contractName: 'flood', - }); + const url = `${nodeUrl}/v2/contracts/${address.replace('.', '/')}/source`; + const res = await fetch(url); + const result = (await res.json()) as { source: string }; return !!result.source; } catch (e) { return false; @@ -124,11 +123,18 @@ async function run() { async function flood() { const accountFloods = flooders.map(async (flooder, n) => { - // const nonce = await getNonce(flooder.stxAddress, network); - const nonces = accountsApi.getAccountNonces({ - principal: flooder.stxAddress, + const { data } = await apiClient.GET('/extended/v1/address/{principal}/nonces', { + params: { + path: { + principal: flooder.stxAddress, + }, + }, }); - const nonce = ((await nonces).last_executed_tx_nonce ?? -1) + 1; + if (!data) { + logger.error(`No nonces found for ${flooder.stxAddress}`); + return; + } + const nonce = (data.last_executed_tx_nonce ?? -1) + 1; logger.info(`Flooder ${n} has nonce ${nonce.toString()}`); // return { ...account, nonce }; let txFloods = new Array(TX_PER_FLOOD).fill(0).map(async (_, i) => { diff --git a/stacking/package.json b/stacking/package.json index 6db960a..1efdee6 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "format": "prettier --write *.ts", - "flood": "dotenvx run -f ./tx-broadcaster.env -- npx tsx flood.ts" + "flood": "dotenvx run -f ./tx-broadcaster.env -- npx tsx flood.ts", + "typecheck": "tsc --noEmit" }, "keywords": [], "author": "", From 188841b388c20c500f84af99c87cb628fc6e74bc Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:41:29 -0700 Subject: [PATCH 08/30] fix: consolidate scripts a bit --- docker-compose.yml | 2 +- stacking/{btc-rpc.ts => btc-helpers.ts} | 66 +++++++++++++++++++++++++ stacking/btc-locking.ts | 63 ----------------------- stacking/btc-staker.ts | 56 ++++++++++----------- stacking/contracts.ts | 7 --- stacking/monitor.ts | 2 +- stacking/pox-5-helpers.ts | 21 ++++---- 7 files changed, 106 insertions(+), 111 deletions(-) rename stacking/{btc-rpc.ts => btc-helpers.ts} (55%) delete mode 100644 stacking/btc-locking.ts delete mode 100644 stacking/contracts.ts diff --git a/docker-compose.yml b/docker-compose.yml index b14f954..6ed2c8b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 190a195f690025a07371cb2a5e84b3f17cc63c92 # feat/epoch-3-5-rc + - &STACKS_BLOCKCHAIN_COMMIT adfcb143a939c8723318fc26252ba3eaba2265b3 # feat/epoch-3-5-rc - &STACKS_API_COMMIT cb5881749553a34704cab62a9631341cb30f2c17 # 8.13.4 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za diff --git a/stacking/btc-rpc.ts b/stacking/btc-helpers.ts similarity index 55% rename from stacking/btc-rpc.ts rename to stacking/btc-helpers.ts index e4c9cdb..b24861c 100644 --- a/stacking/btc-rpc.ts +++ b/stacking/btc-helpers.ts @@ -1,3 +1,69 @@ +import * as BTC from '@scure/btc-signer'; +import { hex } from '@scure/base'; +import { createAddress } from '@stacks/transactions'; + +export const REGTEST_NETWORK = { + bech32: 'bcrt', + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, +} as const; + +// -- Script construction -- + +export function getUnlockBytes(pubKeyHex: string): Uint8Array { + return BTC.Script.encode([hex.decode(pubKeyHex), 'CHECKSIG']); +} + +export function serializeLockupScript({ + stacker, + unlockBurnHeight, + unlockBytes, +}: { + stacker: string; + unlockBurnHeight: bigint; + unlockBytes: Uint8Array; +}): Uint8Array { + const addr = createAddress(stacker); + return BTC.Script.encode([ + new Uint8Array([5, addr.version, ...hex.decode(addr.hash160)]), + 'DROP', + Number(unlockBurnHeight), + 'CHECKLOCKTIMEVERIFY', + 'DROP', + unlockBytes, + ]); +} + +export function toWitnessOutput(script: Uint8Array): Uint8Array { + return BTC.OutScript.encode(BTC.p2wsh({ type: 'wsh', script })); +} + +// -- Unlock height calculation -- + +export function calculateUnlockBurnHeight( + currentCycle: number, + numCycles: number, + rewardCycleLength: number, +): bigint { + const startCycle = currentCycle + 1; + const lastCycle = startCycle + numCycles; + const lastCycleStartHeight = (lastCycle * rewardCycleLength) + 1; + return BigInt(lastCycleStartHeight) + (BigInt(rewardCycleLength) / 2n); +} + +// -- P2WSH address from lock script -- + +export function getLockingAddress(lockScript: Uint8Array): string { + const p2wsh = BTC.p2wsh({ + script: lockScript, + type: 'sh', + }, REGTEST_NETWORK); + return p2wsh.address!; +} + +// -- Bitcoin RPC -- + const host = process.env.BITCOIN_RPC_HOST ?? 'bitcoind'; const port = process.env.BITCOIN_RPC_PORT ?? '18443'; const user = process.env.BITCOIN_RPC_USER ?? 'btc'; diff --git a/stacking/btc-locking.ts b/stacking/btc-locking.ts deleted file mode 100644 index f17c18e..0000000 --- a/stacking/btc-locking.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as BTC from '@scure/btc-signer'; -import { hex } from '@scure/base'; -import { createAddress } from '@stacks/transactions'; - -export const REGTEST_NETWORK = { - bech32: 'bcrt', - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, -} as const; - -// -- Script construction -- - -export function getUnlockBytes(pubKeyHex: string): Uint8Array { - return BTC.Script.encode([hex.decode(pubKeyHex), 'CHECKSIG']); -} - -export function serializeLockupScript({ - stacker, - unlockBurnHeight, - unlockBytes, -}: { - stacker: string; - unlockBurnHeight: bigint; - unlockBytes: Uint8Array; -}): Uint8Array { - const addr = createAddress(stacker); - return BTC.Script.encode([ - new Uint8Array([5, addr.version, ...hex.decode(addr.hash160)]), - 'DROP', - Number(unlockBurnHeight), - 'CHECKLOCKTIMEVERIFY', - 'DROP', - unlockBytes, - ]); -} - -export function toWitnessOutput(script: Uint8Array): Uint8Array { - return BTC.OutScript.encode(BTC.p2wsh({ type: 'wsh', script })); -} - -// -- Unlock height calculation -- - -export function calculateUnlockBurnHeight( - currentCycle: number, - numCycles: number, - rewardCycleLength: number, -): bigint { - const startCycle = currentCycle + 1; - const lastCycle = startCycle + numCycles; - const lastCycleStartHeight = (lastCycle * rewardCycleLength) + 1; - return BigInt(lastCycleStartHeight) + (BigInt(rewardCycleLength) / 2n); -} - -// -- P2WSH address from lock script -- - -export function getLockingAddress(lockScript: Uint8Array): string { - const p2wsh = BTC.p2wsh({ - script: lockScript, - type: 'sh', - }, REGTEST_NETWORK); - return p2wsh.address!; -} diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index a08935f..36316f9 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -1,39 +1,35 @@ import { - makeContractCall, - broadcastTransaction, - bufferCV, - uintCV, - tupleCV, - AnchorMode, - createAddress, - someCV, + makeContractCall, + broadcastTransaction, + AnchorMode, + createAddress } from '@stacks/transactions'; import { hex } from '@scure/base'; -import { Pox4SignatureTopic, PoxInfo } from '@stacks/stacking'; +import { PoxInfo } from '@stacks/stacking'; import { - accounts, - maxAmount, - parseEnvInt, - waitForSetup, - logger, - burnBlockToRewardCycle, - network, - POX_REWARD_LENGTH, - type Account, - EPOCH_35_START, - WALLET_NAME, - waitForTxConfirmed, + accounts, + maxAmount, + parseEnvInt, + waitForSetup, + logger, + burnBlockToRewardCycle, + network, + POX_REWARD_LENGTH, + type Account, + EPOCH_35_START, + WALLET_NAME, + waitForTxConfirmed, } from './common.js'; -import { createOrLoadWallet, getNewAddress, listUnspent, sendToAddress } from './btc-rpc.js'; import { - getUnlockBytes, - serializeLockupScript, - calculateUnlockBurnHeight, - getLockingAddress, -} from './btc-locking.js'; -import { pox5 } from './contracts.js'; -import { TESTNET_BURN_ADDRESS } from '@clarigen/core'; -import { signSignerKeyGrant } from './pox-5-helpers.js'; + getUnlockBytes, + serializeLockupScript, + calculateUnlockBurnHeight, + getLockingAddress, + createOrLoadWallet, + listUnspent, + sendToAddress +} from './btc-helpers.js'; +import { signSignerKeyGrant, pox5 } from './pox-5-helpers.js'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); const postTxWait = parseEnvInt('POST_TX_WAIT', true); diff --git a/stacking/contracts.ts b/stacking/contracts.ts deleted file mode 100644 index d9f1a60..0000000 --- a/stacking/contracts.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { contractFactory, TESTNET_BURN_ADDRESS, ClarigenClient } from '@clarigen/core'; -import { contracts } from './clarigen-types.js'; -import { network } from './common.js'; - -export const clarigenClient = new ClarigenClient(network); - -export const pox5 = contractFactory(contracts.pox5, `${TESTNET_BURN_ADDRESS}.pox-5`); \ No newline at end of file diff --git a/stacking/monitor.ts b/stacking/monitor.ts index 73deb2c..fbdd2ae 100644 --- a/stacking/monitor.ts +++ b/stacking/monitor.ts @@ -1,4 +1,4 @@ -import { bitcoinRPC } from './btc-rpc.js'; +import { bitcoinRPC } from './btc-helpers.js'; import { accounts, nodeUrl, diff --git a/stacking/pox-5-helpers.ts b/stacking/pox-5-helpers.ts index 2045553..5b1a97c 100644 --- a/stacking/pox-5-helpers.ts +++ b/stacking/pox-5-helpers.ts @@ -1,17 +1,20 @@ import * as BTC from '@scure/btc-signer'; import { - Cl, - createAddress, - encodeStructuredDataBytes, - getAddressFromPublicKey, - signWithKey, + Cl, + createAddress, + encodeStructuredDataBytes, + getAddressFromPublicKey, + signWithKey, } from '@stacks/transactions'; import { hex } from '@scure/base'; -import { projectErrors, projectFactory } from '@clarigen/core'; -import { accounts, project } from './clarigen-types.js'; +import { ClarigenClient, contractFactory, projectErrors, TESTNET_BURN_ADDRESS } from '@clarigen/core'; +import { contracts, project } from './clarigen-types.js'; import { sha256 } from '@noble/hashes/sha2.js'; -import { secp256k1 } from '@noble/curves/secp256k1.js'; -import { pox5 } from './contracts.js'; +import { network } from './common.js'; + +export const clarigenClient = new ClarigenClient(network); + +export const pox5 = contractFactory(contracts.pox5, `${TESTNET_BURN_ADDRESS}.pox-5`); export const errorCodes = projectErrors(project).pox5; From c8ee6a5bf359228308e73f906db53159ae85bf9e Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:15:01 -0700 Subject: [PATCH 09/30] fix: sign with correct chain id --- stacking/pox-5-helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stacking/pox-5-helpers.ts b/stacking/pox-5-helpers.ts index 5b1a97c..fe61798 100644 --- a/stacking/pox-5-helpers.ts +++ b/stacking/pox-5-helpers.ts @@ -10,7 +10,7 @@ import { hex } from '@scure/base'; import { ClarigenClient, contractFactory, projectErrors, TESTNET_BURN_ADDRESS } from '@clarigen/core'; import { contracts, project } from './clarigen-types.js'; import { sha256 } from '@noble/hashes/sha2.js'; -import { network } from './common.js'; +import { CHAIN_ID, network } from './common.js'; export const clarigenClient = new ClarigenClient(network); @@ -76,7 +76,7 @@ export function signSignerKeyGrant({ domain: Cl.tuple({ name: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.name), version: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.version), - 'chain-id': Cl.uint(pox5.constants.pOX_5_SIGNER_DOMAIN.chainId), + 'chain-id': Cl.uint(CHAIN_ID), }), }); const data = signWithKey(signerSk, hex.encode(sha256(fullMessage))); From 02d8c437dc6f01df15b0a20b5f7b12db17b9612e Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 14 May 2026 14:26:24 -0700 Subject: [PATCH 10/30] feat: update to use epoch 4.0 --- .gitignore | 3 + Dockerfile.stacker | 3 + Dockerfile.stacks-api | 2 +- docker-compose.yml | 37 +- stacking/Clarinet.toml | 8 + stacking/btc-staker.ts | 285 +- stacking/clarigen-types.ts | 1729 +++++++-- stacking/common.ts | 43 +- stacking/contracts/pox-5-signer.clar | 296 ++ stacking/contracts/pox-5.clar | 3150 +++++++++++++---- stacking/deployments/default.simnet-plan.yaml | 23 + stacking/deployments/sbtc.devnet-plan.yaml | 34 + stacking/package.json | 7 +- stacking/pox-5-helpers.ts | 41 +- stacking/tests/helpers.test.ts | 6 + stacks-krypton-miner.toml | 5 +- 16 files changed, 4513 insertions(+), 1159 deletions(-) create mode 100644 stacking/contracts/pox-5-signer.clar create mode 100644 stacking/deployments/sbtc.devnet-plan.yaml create mode 100644 stacking/tests/helpers.test.ts diff --git a/.gitignore b/.gitignore index ef5738c..f0d8cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ stacking/node_modules package-lock.json +.cache +node_modules +ai \ No newline at end of file diff --git a/Dockerfile.stacker b/Dockerfile.stacker index 160981b..21eecbd 100644 --- a/Dockerfile.stacker +++ b/Dockerfile.stacker @@ -6,6 +6,9 @@ WORKDIR /root COPY ./stacking/package.json /root/ RUN npm i +COPY ./stacking/deployments/*.yaml /root/deployments/ +COPY ./stacking/contracts/*.clar /root/contracts/ +COPY ./stacking/.cache/requirements/*.clar /root/.cache/requirements/ COPY ./stacking/*.ts /root/ CMD ["npx", "tsx", "/root/stacking.ts"] \ No newline at end of file diff --git a/Dockerfile.stacks-api b/Dockerfile.stacks-api index 61dadd9..8757a5b 100644 --- a/Dockerfile.stacks-api +++ b/Dockerfile.stacks-api @@ -12,7 +12,7 @@ RUN git init && \ git reset --hard FETCH_HEAD && \ git fetch --all --tags -RUN rm ".env" +# RUN rm ".env" RUN git describe --tags --abbrev=0 || git -c user.name='user' -c user.email='email' tag vNext RUN echo "GIT_TAG=$(git tag --points-at HEAD)" >> .env diff --git a/docker-compose.yml b/docker-compose.yml index 6ed2c8b..52a403e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT adfcb143a939c8723318fc26252ba3eaba2265b3 # feat/epoch-3-5-rc - - &STACKS_API_COMMIT cb5881749553a34704cab62a9631341cb30f2c17 # 8.13.4 + - &STACKS_BLOCKCHAIN_COMMIT 01550f7de9a42102ac7049e8708d00de5eca7aa2 # feat/epoch-4-rc + - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za - &BITCOIN_PEER_PORT 18444 @@ -8,9 +8,9 @@ x-common-vars: - &BITCOIN_RPC_USER btc - &BITCOIN_RPC_PASS btc - &MINE_INTERVAL ${MINE_INTERVAL:-1s} - - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-2s} # 5 second bitcoin block times in epoch 2.5 - - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-2s} # 30 second bitcoin block times in epoch 3 - - &MINE_INTERVAL_EPOCH35 ${MINE_INTERVAL_EPOCH35:-2s} # 5 second bitcoin block times in epoch 3.5 + - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-2s} # bitcoin block times in epoch 2.5 + - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-2s} # bitcoin block times in epoch 3 + - &MINE_INTERVAL_EPOCH40 ${MINE_INTERVAL_EPOCH40:-60s} # bitcoin block times in epoch 4 - &NAKAMOTO_BLOCK_INTERVAL 2 # seconds to wait between issuing stx-transfer transactions (which triggers Nakamoto block production) - &STACKS_20_HEIGHT ${STACKS_20_HEIGHT:-0} - &STACKS_2_05_HEIGHT ${STACKS_2_05_HEIGHT:-102} @@ -25,8 +25,9 @@ x-common-vars: - &STACKS_32_HEIGHT ${STACKS_32_HEIGHT:-133} - &STACKS_33_HEIGHT ${STACKS_33_HEIGHT:-134} - &STACKS_34_HEIGHT ${STACKS_34_HEIGHT:-135} - - &STACKS_35_HEIGHT ${STACKS_35_HEIGHT:-141} + - &STACKS_40_HEIGHT ${STACKS_40_HEIGHT:-141} - &STACKING_CYCLES ${STACKING_CYCLES:-1} # number of cycles to stack-stx or stack-extend for + - &STACKING_CYCLES_POX_5 ${STACKING_CYCLES_POX_5:-1} # number of cycles to stack-stx or stack-extend for - &POX_PREPARE_LENGTH ${POX_PREPARE_LENGTH:-5} - &POX_REWARD_LENGTH ${POX_REWARD_LENGTH:-20} - &REWARD_RECIPIENT ${REWARD_RECIPIENT:-STQM73RQC4EX0A07KWG1J5ECZJYBZS4SJ4ERC6WN} # priv: 6ad9cadb42d4edbfbe0c5bfb3b8a4125ddced021c4174f829b714ccbf527f02001 @@ -76,11 +77,11 @@ services: MINE_INTERVAL: *MINE_INTERVAL MINE_INTERVAL_EPOCH3: *MINE_INTERVAL_EPOCH3 MINE_INTERVAL_EPOCH25: *MINE_INTERVAL_EPOCH25 - MINE_INTERVAL_EPOCH35: *MINE_INTERVAL_EPOCH35 + MINE_INTERVAL_EPOCH40: *MINE_INTERVAL_EPOCH40 INIT_BLOCKS: 101 STACKS_30_HEIGHT: *STACKS_30_HEIGHT STACKS_25_HEIGHT: *STACKS_25_HEIGHT - STACKS_35_HEIGHT: *STACKS_35_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT entrypoint: - /bin/bash - -c @@ -228,9 +229,9 @@ services: SLEEP_DURATION=$${MINE_INTERVAL} BLOCK_HEIGHT=$$(bitcoin-cli -rpcconnect=bitcoind getblockcount) - if [ "$${BLOCK_HEIGHT}" -ge "$${STACKS_35_HEIGHT}" ]; then - echo "In Epoch3.5, sleeping for $${MINE_INTERVAL_EPOCH35} ..." - SLEEP_DURATION=$${MINE_INTERVAL_EPOCH35} + if [ "$${BLOCK_HEIGHT}" -ge "$${STACKS_40_HEIGHT}" ]; then + echo "In Epoch4.0, sleeping for $${MINE_INTERVAL_EPOCH40} ..." + SLEEP_DURATION=$${MINE_INTERVAL_EPOCH40} elif [ "$${BLOCK_HEIGHT}" -ge $$(( $${STACKS_30_HEIGHT} - 1 )) ]; then echo "In Epoch3, sleeping for $${MINE_INTERVAL_EPOCH3} ..." SLEEP_DURATION=$${MINE_INTERVAL_EPOCH3} @@ -283,7 +284,7 @@ services: STACKS_32_HEIGHT: *STACKS_32_HEIGHT STACKS_33_HEIGHT: *STACKS_33_HEIGHT STACKS_34_HEIGHT: *STACKS_34_HEIGHT - STACKS_35_HEIGHT: *STACKS_35_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH REWARD_RECIPIENT: *REWARD_RECIPIENT @@ -314,10 +315,11 @@ services: STACKS_CORE_RPC_HOST: stacks-node STACKS_CORE_RPC_PORT: 20443 STACKING_CYCLES: *STACKING_CYCLES + STACKING_CYCLES_POX_5: *STACKING_CYCLES_POX_5 STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT STACKS_30_HEIGHT: *STACKS_30_HEIGHT - STACKS_35_HEIGHT: *STACKS_35_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH STACKS_CHAIN_ID: *STACKS_CHAIN_ID @@ -337,10 +339,11 @@ services: STACKS_CORE_RPC_HOST: stacks-api STACKS_CORE_RPC_PORT: 3999 STACKING_CYCLES: *STACKING_CYCLES + STACKING_CYCLES_POX_5: *STACKING_CYCLES_POX_5 STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT STACKS_30_HEIGHT: *STACKS_30_HEIGHT - STACKS_35_HEIGHT: *STACKS_35_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH STACKS_CHAIN_ID: *STACKS_CHAIN_ID @@ -372,10 +375,11 @@ services: STACKS_CORE_RPC_HOST: stacks-api STACKS_CORE_RPC_PORT: 3999 STACKING_CYCLES: *STACKING_CYCLES + STACKING_CYCLES_POX_5: *STACKING_CYCLES_POX_5 STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT STACKS_30_HEIGHT: *STACKS_30_HEIGHT - STACKS_35_HEIGHT: *STACKS_35_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH EXIT_FROM_MONITOR: *EXIT_FROM_MONITOR @@ -403,7 +407,7 @@ services: STACKS_30_HEIGHT: *STACKS_30_HEIGHT ACCOUNT_KEYS: 0d2f965b472a82efd5a96e6513c8b9f7edc725d5c96c7d35d6c722cedeb80d1b01,975b251dd7809469ef0c26ec3917971b75c51cd73a022024df4bf3b232cc2dc001,c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01 STACKS_25_HEIGHT: *STACKS_25_HEIGHT - STACKS_35_HEIGHT: *STACKS_35_HEIGHT + STACKS_40_HEIGHT: *STACKS_40_HEIGHT POX_PREPARE_LENGTH: *POX_PREPARE_LENGTH POX_REWARD_LENGTH: *POX_REWARD_LENGTH STACKS_CHAIN_ID: *STACKS_CHAIN_ID @@ -462,6 +466,7 @@ services: PG_DATABASE: stacks_blockchain_api PG_SCHEMA: public STACKS_CORE_RPC_HOST: stacks-node + STACKS_CORE_PROXY_HOST: stacks-node STACKS_CORE_RPC_PORT: 20443 BTC_RPC_HOST: http://bitcoind BTC_RPC_PORT: 18443 diff --git a/stacking/Clarinet.toml b/stacking/Clarinet.toml index e9563d4..5001815 100644 --- a/stacking/Clarinet.toml +++ b/stacking/Clarinet.toml @@ -2,6 +2,9 @@ [project] name = "core-contracts" +[[project.requirements]] +contract_id = "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal" + [repl] costs_version = 1 @@ -10,6 +13,11 @@ path = "./contracts/pox-5.clar" clarity_version = 4 epoch = 3.3 +[contracts.pox-5-signer] +path = "./contracts/pox-5-signer.clar" +clarity_version = 4 +epoch = 3.3 + [repl.analysis.lints] unused_const = "warn" unused_data_var = "warn" diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 36316f9..6cbd681 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -1,39 +1,40 @@ import { - makeContractCall, - broadcastTransaction, - AnchorMode, - createAddress + makeContractCall, + broadcastTransaction, + AnchorMode, + makeContractDeploy, } from '@stacks/transactions'; import { hex } from '@scure/base'; import { PoxInfo } from '@stacks/stacking'; import { - accounts, - maxAmount, - parseEnvInt, - waitForSetup, - logger, - burnBlockToRewardCycle, - network, - POX_REWARD_LENGTH, - type Account, - EPOCH_35_START, - WALLET_NAME, - waitForTxConfirmed, + accounts, + parseEnvInt, + waitForSetup, + logger, + burnBlockToRewardCycle, + network, + POX_REWARD_LENGTH, + type Account, + EPOCH_40_START, + WALLET_NAME, + waitForTxConfirmed, + EPOCH_30_START, + fetchAccount, } from './common.js'; import { - getUnlockBytes, - serializeLockupScript, - calculateUnlockBurnHeight, - getLockingAddress, - createOrLoadWallet, - listUnspent, - sendToAddress + getUnlockBytes, + serializeLockupScript, + calculateUnlockBurnHeight, + getLockingAddress, + createOrLoadWallet, + listUnspent, + sendToAddress, } from './btc-helpers.js'; -import { signSignerKeyGrant, pox5 } from './pox-5-helpers.js'; +import { signSignerKeyGrant, pox5, pox5Signer } from './pox-5-helpers.js'; +import { readFile } from 'node:fs/promises'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); -const postTxWait = parseEnvInt('POST_TX_WAIT', true); -const stakingCycles = parseEnvInt('STACKING_CYCLES', true); +const stakingCyclesPox5 = parseEnvInt('STACKING_CYCLES_POX_5', true); const lockAmountSats = BigInt(parseEnvInt('BTC_LOCK_AMOUNT_SATS', false) ?? 10_000_000); let txFee = parseEnvInt('STACKING_FEE', false) ?? 1_000_000; @@ -58,58 +59,15 @@ async function initBtcWallet() { } } -async function submitSignerKeyGrant(account: Account) { - const authId = 1n; - const signature = signSignerKeyGrant({ - staker: account.stxAddress, - poxAddr: null, - authId, - signerSk: hex.decode(account.signerPrivKey), - }); - - const tx = await makeContractCall({ - ...pox5.grantSignerKey({ - signerKey: hex.decode(account.signerPubKey), - staker: account.stxAddress, - poxAddr: null, - authId, - signerSig: signature, - }), - senderKey: account.privKey, - network, - }) - const result = await broadcastTransaction({ - transaction: tx, - network, - }); - account.logger.info({ ...result }, 'L2 signer key grant tx broadcast'); - return result; -} - // -- L2: Stacks contract calls -- -async function submitStake( - account: Account, - poxInfo: PoxInfo, - unlockBytes: Uint8Array, -) { - const authId = Math.floor(Math.random() * 0xffffffffffff); - - const poxAddr = createAddress(account.stxAddress); - +async function submitStake(account: Account, poxInfo: PoxInfo) { const stakeFnCall = pox5.stake({ - amountUstx: 1000_000000n, - poxAddr: { - version: new Uint8Array([1]), - hashbytes: hex.decode(poxAddr.hash160), - }, startBurnHt: poxInfo.current_burnchain_block_height!, - signerSig: null, - signerKey: hex.decode(account.signerPubKey), - maxAmount, - authId, - numCycles: stakingCycles, - unlockBytes: unlockBytes, + amountUstx: 1000_000000n, + numCycles: stakingCyclesPox5, + signerManager: account.signerManager, + signerCalldata: null, }); const tx = await makeContractCall({ @@ -122,28 +80,27 @@ async function submitStake( transaction: tx, network, }); - account.logger.info({ ...result }, 'L2 stake tx broadcast'); + if ('reason' in result) { + account.logger.error( + { + ...result, + }, + `Error staking: ${result.reason}` + ); + throw new Error(`Error staking: ${result.reason}`); + } + account.logger.info({ ...result }, 'stake tx broadcast'); return result; } -async function submitStakeExtend(account: Account, poxInfo: any, unlockBytes: Uint8Array) { - const authId = Math.floor(Math.random() * 0xffffffffffff); - - const poxAddr = createAddress(account.stxAddress); - +async function submitStakeExtend(account: Account) { const txOptions = { - ...pox5.stakeExtend({ - amountUstx: 1000_000000n, - poxAddr: { - version: new Uint8Array([1]), - hashbytes: hex.decode(poxAddr.hash160), - }, - signerSig: null, - signerKey: hex.decode(account.signerPubKey), - maxAmount, - authId, - numCycles: stakingCycles, - unlockBytes, + ...pox5.stakeUpdate({ + amountIncrease: 0n, + cyclesToExtend: stakingCyclesPox5, + signerManager: account.signerManager, + oldSignerManager: account.signerManager, + signerCalldata: null, }), senderKey: account.privKey, network, @@ -173,19 +130,26 @@ async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockB const amountBtc = Number(lockAmountSats) / 1e8; const txid = await sendToAddress(WALLET_NAME, address, amountBtc); - account.logger.info({ txid, address, amountBtc, unlockBurnHeight: unlockBurnHeight.toString() }, 'L1 BTC lock tx broadcast'); + account.logger.info( + { txid, address, amountBtc, unlockBurnHeight: unlockBurnHeight.toString() }, + 'L1 BTC lock tx broadcast' + ); return txid; } // -- Main loop -- -let lastStakedCycle = 0; - -let hasGrantedSignerKey = false; +const grantedSignerKeys = new Set(); +let hasDeployedSBTC = false; async function run() { const poxInfo = await accounts[0]!.client.getPoxInfo(); - if (poxInfo.current_burnchain_block_height! <= EPOCH_35_START) { + + if (poxInfo.current_burnchain_block_height! > EPOCH_30_START + 1 && !hasDeployedSBTC) { + await deploySBTC(accounts[0]!); + hasDeployedSBTC = true; + } + if (poxInfo.current_burnchain_block_height! < EPOCH_40_START) { // logger.info({ burnHeight: poxInfo.current_burnchain_block_height }, 'Not on epoch 3.5 yet, skipping'); return; } @@ -194,14 +158,10 @@ async function run() { const accountInfos = await Promise.all( accounts.map(async a => { - const info = await a.client.getAccountStatus(); - return { - ...a, - unlockHeight: Number(info.unlock_height), - lockedAmount: BigInt(info.locked), - balance: BigInt(info.balance), - }; - }), + a.logger.info({ address: a.stxAddress }, 'Getting account status'); + const info = await fetchAccount(a.stxAddress); + return { ...a, ...info }; + }) ); const nowCycle = burnBlockToRewardCycle(poxInfo.current_burnchain_block_height ?? 0); @@ -210,15 +170,62 @@ async function run() { for (const account of accountInfos) { const unlockBytes = getUnlockBytes(account.pubKey); - const unlockBurnHeight = calculateUnlockBurnHeight(currentCycle, stakingCycles, POX_REWARD_LENGTH); + const unlockBurnHeight = calculateUnlockBurnHeight( + currentCycle, + stakingCyclesPox5, + POX_REWARD_LENGTH + ); + + if (!grantedSignerKeys.has(account.signerManager)) { + const authId = 2n; + const signature = signSignerKeyGrant({ + signerManager: account.signerManager, + authId, + signerSk: hex.decode(account.signerPrivKey), + }); - if (!hasGrantedSignerKey) { - const txResult = await submitSignerKeyGrant(account); - if ('error' in txResult) { - logger.error({ ...txResult }, 'Error granting signer key'); - continue; + const signerManager = await readFile('./contracts/pox-5-signer.clar', 'utf8'); + const deployTx = await makeContractDeploy({ + senderKey: account.privKey, + network, + contractName: 'signer-manager', + codeBody: signerManager + .replaceAll(' .pox-5', ` '${pox5.identifier}`) + .replaceAll( + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4', + 'ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP' + ), + }); + const deployResult = await broadcastTransaction({ + transaction: deployTx, + network, + }); + const exists = 'reason' in deployResult && deployResult.reason === 'ContractAlreadyExists'; + if (!exists) { + if ('reason' in deployResult) { + throw new Error(`Error deploying signer manager: ${deployResult.reason}`); + } + account.logger.info({ ...deployResult }, 'Deployed signer manager'); + await waitForTxConfirmed(deployResult.txid); } - await waitForTxConfirmed(txResult.txid); + + const registerSelf = await makeContractCall({ + ...pox5Signer(account.signerManager).registerSelf({ + signerManager: account.signerManager, + signerKey: hex.decode(account.signerPubKey), + authId, + signerSig: signature, + }), + senderKey: account.privKey, + network, + }); + const registerSelfResult = await broadcastTransaction({ + transaction: registerSelf, + network, + }); + account.logger.info({ ...registerSelfResult }, 'Registered self'); + await waitForTxConfirmed(registerSelfResult.txid); + grantedSignerKeys.add(account.signerManager); } if (account.lockedAmount === 0n) { @@ -228,9 +235,8 @@ async function run() { unlockBurnHeight: unlockBurnHeight.toString(), }); - const stakeResult = await submitStake(account, poxInfo, unlockBytes); + const stakeResult = await submitStake(account, poxInfo); txIdsToWait.push(stakeResult.txid); - // await new Promise(r => setTimeout(r, postTxWait * 1000)); await submitBtcLock(account, unlockBurnHeight, unlockBytes); continue; @@ -239,11 +245,13 @@ async function run() { const unlockCycle = burnBlockToRewardCycle(account.unlockHeight); if (unlockCycle === nowCycle) { - account.logger.info({ unlockHeight: account.unlockHeight, nowCycle, unlockCycle }, 'Extending stake...'); + account.logger.info( + { unlockHeight: account.unlockHeight, nowCycle, unlockCycle }, + 'Extending stake...' + ); - const stakeExtendResult = await submitStakeExtend(account, poxInfo, unlockBytes); + const stakeExtendResult = await submitStakeExtend(account); txIdsToWait.push(stakeExtendResult.txid); - // await new Promise(r => setTimeout(r, postTxWait * 1000)); await submitBtcLock(account, unlockBurnHeight, unlockBytes); continue; @@ -252,8 +260,47 @@ async function run() { account.logger.info({ nowCycle, unlockCycle }, 'Staked through next cycle, skipping'); } await Promise.all(txIdsToWait.map(waitForTxConfirmed)); - lastStakedCycle = nowCycle; - hasGrantedSignerKey = true; +} + +async function deploySBTC(account: Account) { + const registry = await readFile( + '.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar', + 'utf8' + ); + const token = await readFile( + '.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar', + 'utf8' + ); + const withdrawal = await readFile( + '.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar', + 'utf8' + ); + + async function deployContract(contract: string, name: string) { + const deployTx = await makeContractDeploy({ + senderKey: accounts[0]!.privKey, + network, + contractName: name, + codeBody: contract, + clarityVersion: 3, + }); + const deployResult = await broadcastTransaction({ + transaction: deployTx, + network, + }); + if ('reason' in deployResult) { + if (deployResult.reason === 'ContractAlreadyExists') { + return; + } + throw new Error(`Error deploying sbtc contract: ${deployResult.reason}`); + } + account.logger.info({ ...deployResult, contractName: name }, 'Deployed sbtc contract'); + await waitForTxConfirmed(deployResult.txid); + } + + await deployContract(registry, 'sbtc-registry'); + await deployContract(token, 'sbtc-token'); + await deployContract(withdrawal, 'sbtc-withdrawal'); } async function loop() { diff --git a/stacking/clarigen-types.ts b/stacking/clarigen-types.ts index 95af735..a39fc84 100644 --- a/stacking/clarigen-types.ts +++ b/stacking/clarigen-types.ts @@ -4,218 +4,255 @@ import type { TypedAbiArg, TypedAbiFunction, TypedAbiMap, TypedAbiVariable, Resp export const contracts = { pox5: { "functions": { - addStakerToNthRewardCycle: {"name":"add-staker-to-nth-reward-cycle","access":"private","args":[{"name":"cycle-index","type":"uint128"},{"name":"params-resp","type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[cycleIndex: TypedAbiArg, paramsResp: TypedAbiArg, "paramsResp">], Response<{ - "firstRewardCycle": bigint; + addStakerToBond: {"name":"add-staker-to-bond","access":"private","args":[{"name":"staker-item","type":{"tuple":[{"name":"max-sats","type":"uint128"},{"name":"staker","type":"principal"}]}},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"sum-max-sats","type":"uint128"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"sum-max-sats","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[stakerItem: TypedAbiArg<{ + "maxSats": number | bigint; "staker": string; +}, "stakerItem">, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "bondIndex": bigint; + "sumMaxSats": bigint; }, bigint>>, - addStakerToRewardCycles: {"name":"add-staker-to-reward-cycles","access":"private","args":[{"name":"staker","type":"principal"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstRewardCycle: TypedAbiArg, numCycles: TypedAbiArg], Response>, addStakerToSetForCycle: {"name":"add-staker-to-set-for-cycle","access":"private","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], Response>, - consumeSignerKeyAuthorization: {"name":"consume-signer-key-authorization","access":"private","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"amount","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, signerSig: TypedAbiArg, signerKey: TypedAbiArg, amount: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Response>, - innerStake: {"name":"inner-stake","access":"private","args":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"start-burn-ht","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg, startBurnHt: TypedAbiArg, poolOrSoloInfo: TypedAbiArg, "poolOrSoloInfo">], Response<{ + addStakerToSignerCycles: {"name":"add-staker-to-signer-cycles","access":"private","args":[{"name":"staker","type":"principal"},{"name":"signer","type":"principal"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"amount-ustx","type":"uint128"},{"name":"is-stx-staking","type":"bool"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, signer: TypedAbiArg, firstRewardCycle: TypedAbiArg, numCycles: TypedAbiArg, amountUstx: TypedAbiArg, isStxStaking: TypedAbiArg], Response<{ "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; - "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; - "unlockCycle": bigint; + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "signer": string; + "staker": string; }, bigint>>, - innerStakeExtend: {"name":"inner-stake-extend","access":"private","args":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg, poolOrSoloInfo: TypedAbiArg, "poolOrSoloInfo">], Response<{ + addStakerToSignerForCycle: {"name":"add-staker-to-signer-for-cycle","access":"private","args":[{"name":"cycle-index","type":"uint128"},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[cycleIndex: TypedAbiArg, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; - "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; - "unlockCycle": bigint; + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "signer": string; + "staker": string; }, bigint>>, - innerStakeUpdate: {"name":"inner-stake-update","access":"private","args":[{"name":"amount-ustx-increase","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstxIncrease: TypedAbiArg, poolOrSoloInfo: TypedAbiArg, "poolOrSoloInfo">], Response<{ - "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; - "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; - "unlockCycle": bigint; + assertActiveBondIncluded: {"name":"assert-active-bond-included","access":"private","args":[{"name":"offset","type":"uint128"},{"name":"acc-res","type":{"response":{"ok":{"tuple":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"calculation-height","type":"uint128"},{"name":"latest-bond-index","type":"uint128"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"calculation-height","type":"uint128"},{"name":"latest-bond-index","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[offset: TypedAbiArg, accRes: TypedAbiArg, "accRes">], Response<{ + "bondPeriods": bigint[]; + "calculationHeight": bigint; + "latestBondIndex": bigint; }, bigint>>, - validateSignerKeyUsage: {"name":"validate-signer-key-usage","access":"private","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"signer-sig-opt","type":{"optional":{"buffer":{"length":65}}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"amount","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"},{"name":"staker","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, signerSigOpt: TypedAbiArg, signerKey: TypedAbiArg, amount: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg, staker: TypedAbiArg], Response>, - grantSignerKey: {"name":"grant-signer-key","access":"public","args":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"staker","type":"principal"},{"name":"pox-addr","type":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}},{"name":"auth-id","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerKey: TypedAbiArg, staker: TypedAbiArg, poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -} | null, "poxAddr">, authId: TypedAbiArg, signerSig: TypedAbiArg], Response>, - registerPool: {"name":"register-pool","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-sig","type":{"buffer":{"length":65}}},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"owner","type":"principal"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, signerKey: TypedAbiArg, poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, signerSig: TypedAbiArg, authId: TypedAbiArg], Response<{ - "owner": string; - "poxAddr": { - "hashbytes": Uint8Array; - "version": Uint8Array; -}; - "signerKey": Uint8Array; + calculateBondRewards: {"name":"calculate-bond-rewards","access":"private","args":[{"name":"bond-index","type":"uint128"},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"available-rewards","type":"uint128"},{"name":"calculation-height","type":"uint128"},{"name":"last-bond-index","type":{"optional":"uint128"}},{"name":"last-bond-stx-value-ratio","type":{"optional":"uint128"}}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"available-rewards","type":"uint128"},{"name":"calculation-height","type":"uint128"},{"name":"last-bond-index","type":{"optional":"uint128"}},{"name":"last-bond-stx-value-ratio","type":{"optional":"uint128"}}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "availableRewards": bigint; + "calculationHeight": bigint; + "lastBondIndex": bigint | null; + "lastBondStxValueRatio": bigint | null; }, bigint>>, - revokeSignerGrant: {"name":"revoke-signer-grant","access":"public","args":[{"name":"staker","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, signerKey: TypedAbiArg], Response>, - setBurnchainParameters: {"name":"set-burnchain-parameters","access":"public","args":[{"name":"first-burn-height","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"begin-pox5-reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[firstBurnHeight: TypedAbiArg, prepareCycleLength: TypedAbiArg, rewardCycleLength: TypedAbiArg, beginPox5RewardCycle: TypedAbiArg], Response>, - stake: {"name":"stake","access":"public","args":[{"name":"amount-ustx","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"start-burn-ht","type":"uint128"},{"name":"signer-sig","type":{"optional":{"buffer":{"length":65}}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, startBurnHt: TypedAbiArg, signerSig: TypedAbiArg, signerKey: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg], Response<{ - "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; - "unlockBurnHeight": bigint; + crystallizeRewards: {"name":"crystallize-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, + getBitcoinTxOutput_q: {"name":"get-bitcoin-tx-output?","access":"private","args":[{"name":"tx-bytes","type":{"buffer":{"length":100000}}},{"name":"output-index","type":"uint128"},{"name":"amount","type":"uint128"},{"name":"script","type":{"buffer":{"length":34}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount","type":"uint128"},{"name":"script","type":{"buffer":{"length":34}}},{"name":"txid","type":{"buffer":{"length":32}}}]},"error":"uint128"}}}} as TypedAbiFunction<[txBytes: TypedAbiArg, outputIndex: TypedAbiArg, amount: TypedAbiArg, script: TypedAbiArg], Response<{ + "amount": bigint; + "script": Uint8Array; + "txid": Uint8Array; +}, bigint>>, + lockSbtc: {"name":"lock-sbtc","access":"private","args":[{"name":"amount","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg], Response>, + matchUintInList: {"name":"match-uint-in-list","access":"private","args":[{"name":"item","type":"uint128"},{"name":"acc","type":{"tuple":[{"name":"found","type":"bool"},{"name":"needle","type":"uint128"}]}}],"outputs":{"type":{"tuple":[{"name":"found","type":"bool"},{"name":"needle","type":"uint128"}]}}} as TypedAbiFunction<[item: TypedAbiArg, acc: TypedAbiArg<{ + "found": boolean; + "needle": number | bigint; +}, "acc">], { + "found": boolean; + "needle": bigint; +}>, + removeStakerFromCycles: {"name":"remove-staker-from-cycles","access":"private","args":[{"name":"staker","type":"principal"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"is-stx-staking","type":"bool"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstRewardCycle: TypedAbiArg, numCycles: TypedAbiArg, isStxStaking: TypedAbiArg], Response<{ + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "staker": string; +}, bigint>>, + removeStakerFromSetForCycle: {"name":"remove-staker-from-set-for-cycle","access":"private","args":[{"name":"stacker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[stacker: TypedAbiArg, cycle: TypedAbiArg], Response>, + removeStakerFromSignerForCycle: {"name":"remove-staker-from-signer-for-cycle","access":"private","args":[{"name":"cycle-index","type":"uint128"},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[cycleIndex: TypedAbiArg, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "firstRewardCycle": bigint; + "isStxStaking": boolean; + "staker": string; +}, bigint>>, + reverseBuff16: {"name":"reverse-buff16","access":"private","args":[{"name":"input","type":{"buffer":{"length":16}}}],"outputs":{"type":{"buffer":{"length":17}}}} as TypedAbiFunction<[input: TypedAbiArg], Uint8Array>, + updateClaimableBondRewards: {"name":"update-claimable-bond-rewards","access":"private","args":[{"name":"bond-index","type":"uint128"},{"name":"accumulator","type":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"signer","type":"principal"},{"name":"total","type":"uint128"}]}}],"outputs":{"type":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"signer","type":"principal"},{"name":"total","type":"uint128"}]}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, accumulator: TypedAbiArg<{ + "bondRewards": { + "bondIndex": number | bigint; + "earned": number | bigint; + "rewardsPerToken": number | bigint; +}[]; + "signer": string; + "total": number | bigint; +}, "accumulator">], { + "bondRewards": { + "bondIndex": bigint; + "earned": bigint; + "rewardsPerToken": bigint; +}[]; + "signer": string; + "total": bigint; +}>, + updateClaimableRewards: {"name":"update-claimable-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, + validateL1Lockup: {"name":"validate-l1-lockup","access":"private","args":[{"name":"lockup","type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"header","type":{"buffer":{"length":80}}},{"name":"height","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}},{"name":"output-index","type":"uint128"},{"name":"tx","type":{"buffer":{"length":100000}}},{"name":"tx-count","type":"uint128"},{"name":"tx-index","type":"uint128"}]}},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"expected-script-hash","type":{"buffer":{"length":34}}},{"name":"sum","type":"uint128"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"expected-script-hash","type":{"buffer":{"length":34}}},{"name":"sum","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[lockup: TypedAbiArg<{ + "amount": number | bigint; + "header": Uint8Array; + "height": number | bigint; + "leafHashes": Uint8Array[]; + "outputIndex": number | bigint; + "tx": Uint8Array; + "txCount": number | bigint; + "txIndex": number | bigint; +}, "lockup">, accumulatorRes: TypedAbiArg, "accumulatorRes">], Response<{ + "expectedScriptHash": Uint8Array; + "sum": bigint; +}, bigint>>, + verifyL1Lockups: {"name":"verify-l1-lockups","access":"private","args":[{"name":"staker","type":"principal"},{"name":"bond-index","type":"uint128"},{"name":"lockups","type":{"tuple":[{"name":"outputs","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"header","type":{"buffer":{"length":80}}},{"name":"height","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}},{"name":"output-index","type":"uint128"},{"name":"tx","type":{"buffer":{"length":100000}}},{"name":"tx-count","type":"uint128"},{"name":"tx-index","type":"uint128"}]},"length":10}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, bondIndex: TypedAbiArg, lockups: TypedAbiArg<{ + "outputs": { + "amount": number | bigint; + "header": Uint8Array; + "height": number | bigint; + "leafHashes": Uint8Array[]; + "outputIndex": number | bigint; + "tx": Uint8Array; + "txCount": number | bigint; + "txIndex": number | bigint; +}[]; "unlockBytes": Uint8Array; - "unlockCycle": bigint; +}, "lockups">], Response>, + verifyMerkleProof: {"name":"verify-merkle-proof","access":"private","args":[{"name":"leaf-hash","type":{"buffer":{"length":32}}},{"name":"root-hash","type":{"buffer":{"length":32}}},{"name":"tx-index","type":"uint128"},{"name":"tx-count","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}}],"outputs":{"type":"bool"}} as TypedAbiFunction<[leafHash: TypedAbiArg, rootHash: TypedAbiArg, txIndex: TypedAbiArg, txCount: TypedAbiArg, leafHashes: TypedAbiArg], boolean>, + allowContractCaller: {"name":"allow-contract-caller","access":"public","args":[{"name":"caller","type":"principal"},{"name":"until-burn-ht","type":{"optional":"uint128"}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[caller: TypedAbiArg, untilBurnHt: TypedAbiArg], Response>, + announceL1EarlyExit: {"name":"announce-l1-early-exit","access":"public","args":[{"name":"staker","type":"principal"},{"name":"old-signer-manager","type":"trait_reference"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, oldSignerManager: TypedAbiArg], Response>, + calculateRewards: {"name":"calculate-rewards","access":"public","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg], Response>, + claimRewards: {"name":"claim-rewards","access":"public","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"bond-totals","type":"uint128"},{"name":"stx-rewards","type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}},{"name":"total-rewards","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg, rewardCycle: TypedAbiArg], Response<{ + "bondRewards": { + "bondIndex": bigint; + "earned": bigint; + "rewardsPerToken": bigint; +}[]; + "bondTotals": bigint; + "stxRewards": { + "earned": bigint; + "rewardsPerToken": bigint; +}; + "totalRewards": bigint; }, bigint>>, - stakeExtend: {"name":"stake-extend","access":"public","args":[{"name":"amount-ustx","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-sig","type":{"optional":{"buffer":{"length":65}}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstx: TypedAbiArg, poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, signerSig: TypedAbiArg, signerKey: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg], Response<{ + disallowContractCaller: {"name":"disallow-contract-caller","access":"public","args":[{"name":"caller","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[caller: TypedAbiArg], Response>, + grantSignerKey: {"name":"grant-signer-key","access":"public","args":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-manager","type":"principal"},{"name":"auth-id","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerKey: TypedAbiArg, signerManager: TypedAbiArg, authId: TypedAbiArg, signerSig: TypedAbiArg], Response>, + registerForBond: {"name":"register-for-bond","access":"public","args":[{"name":"bond-index","type":"uint128"},{"name":"signer-manager","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"btc-lockup","type":{"response":{"ok":{"tuple":[{"name":"outputs","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"header","type":{"buffer":{"length":80}}},{"name":"height","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}},{"name":"output-index","type":"uint128"},{"name":"tx","type":{"buffer":{"length":100000}}},{"name":"tx-count","type":"uint128"},{"name":"tx-index","type":"uint128"}]},"length":10}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]},"error":"uint128"}}},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"bond-index","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, signerManager: TypedAbiArg, amountUstx: TypedAbiArg, btcLockup: TypedAbiArg, "btcLockup">, signerCalldata: TypedAbiArg], Response<{ "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; + "bondIndex": bigint; + "firstRewardCycle": bigint; + "signer": string; + "staker": string; "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; "unlockCycle": bigint; }, bigint>>, - stakeExtendPooled: {"name":"stake-extend-pooled","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg], Response<{ - "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response, signerKey: TypedAbiArg], Response<{ + "signer": string; "signerKey": Uint8Array; -}>; - "stacker": string; - "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; - "unlockCycle": bigint; }, bigint>>, - stakePooled: {"name":"stake-pooled","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"start-burn-ht","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, amountUstx: TypedAbiArg, numCycles: TypedAbiArg, unlockBytes: TypedAbiArg, startBurnHt: TypedAbiArg], Response<{ + revokeSignerGrant: {"name":"revoke-signer-grant","access":"public","args":[{"name":"signer-manager","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg], Response>, + setBondAdmin: {"name":"set-bond-admin","access":"public","args":[{"name":"new-admin","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newAdmin: TypedAbiArg], Response>, + setBurnchainParameters: {"name":"set-burnchain-parameters","access":"public","args":[{"name":"first-burn-height","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"begin-pox5-reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[firstBurnHeight: TypedAbiArg, prepareCycleLength: TypedAbiArg, rewardCycleLength: TypedAbiArg, beginPox5RewardCycle: TypedAbiArg], Response>, + setupBond: {"name":"setup-bond","access":"public","args":[{"name":"bond-index","type":"uint128"},{"name":"target-rate","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"early-unlock-admin","type":"principal"},{"name":"allowlist","type":{"list":{"type":{"tuple":[{"name":"max-sats","type":"uint128"},{"name":"staker","type":"principal"}]},"length":1000}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"max-allocation-sats","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, targetRate: TypedAbiArg, stxValueRatio: TypedAbiArg, minUstxRatio: TypedAbiArg, earlyUnlockSigners: TypedAbiArg, earlyUnlockAdmin: TypedAbiArg, allowlist: TypedAbiArg<{ + "maxSats": number | bigint; + "staker": string; +}[], "allowlist">], Response<{ + "bondIndex": bigint; + "earlyUnlockSigners": Uint8Array; + "maxAllocationSats": bigint; + "minUstxRatio": bigint; + "stxValueRatio": bigint; + "targetRate": bigint; +}, bigint>>, + stake: {"name":"stake","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"start-burn-ht","type":"uint128"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycle","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, amountUstx: TypedAbiArg, numCycles: TypedAbiArg, startBurnHt: TypedAbiArg, signerCalldata: TypedAbiArg], Response<{ "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; + "firstRewardCycle": bigint; + "numCycle": bigint; + "signer": string; + "staker": string; "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; "unlockCycle": bigint; }, bigint>>, - stakeUpdate: {"name":"stake-update","access":"public","args":[{"name":"amount-ustx-increase","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-sig","type":{"optional":{"buffer":{"length":65}}}},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[amountUstxIncrease: TypedAbiArg, poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, signerKey: TypedAbiArg, signerSig: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Response<{ + stakeUpdate: {"name":"stake-update","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"old-signer-manager","type":"trait_reference"},{"name":"cycles-to-extend","type":"uint128"},{"name":"amount-increase","type":"uint128"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"prev-unlock-height","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, oldSignerManager: TypedAbiArg, cyclesToExtend: TypedAbiArg, amountIncrease: TypedAbiArg, signerCalldata: TypedAbiArg], Response<{ "amountUstx": bigint; "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; + "prevUnlockHeight": bigint; + "signer": string; + "staker": string; "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; "unlockCycle": bigint; }, bigint>>, - stakeUpdatePooled: {"name":"stake-update-pooled","access":"public","args":[{"name":"pool-owner","type":"trait_reference"},{"name":"amount-ustx-increase","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"stacker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[poolOwner: TypedAbiArg, amountUstxIncrease: TypedAbiArg], Response<{ + unstake: {"name":"unstake","access":"public","args":[{"name":"old-signer-manager","type":"trait_reference"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-cycle","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[oldSignerManager: TypedAbiArg], Response<{ "amountUstx": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "stacker": string; + "firstRewardCycle": bigint; + "staker": string; "unlockBurnHeight": bigint; - "unlockBytes": Uint8Array; "unlockCycle": bigint; }, bigint>>, + unstakeSbtc: {"name":"unstake-sbtc","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"amount-to-withdrawal-sats","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"new-amount-sats","type":"uint128"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, amountToWithdrawalSats: TypedAbiArg], Response<{ + "newAmountSats": bigint; + "signer": string; + "staker": string; +}, bigint>>, + updateBondRegistration: {"name":"update-bond-registration","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"old-signer-manager","type":"trait_reference"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, oldSignerManager: TypedAbiArg, signerCalldata: TypedAbiArg], Response>, + assertAllActiveBondsIncluded: {"name":"assert-all-active-bonds-included","access":"read_only","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"calculation-height","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg, calculationHeight: TypedAbiArg], Response>, + bondPeriodToBurnHeight: {"name":"bond-period-to-burn-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + bondPeriodToRewardCycle: {"name":"bond-period-to-reward-cycle","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + burnHeightToDistributionIndex: {"name":"burn-height-to-distribution-index","access":"read_only","args":[{"name":"height","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[height: TypedAbiArg], bigint>, burnHeightToRewardCycle: {"name":"burn-height-to-reward-cycle","access":"read_only","args":[{"name":"height","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[height: TypedAbiArg], bigint>, - checkPoxAddr: {"name":"check-pox-addr","access":"read_only","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">], Response>, - checkPoxAddrHashbytes: {"name":"check-pox-addr-hashbytes","access":"read_only","args":[{"name":"version","type":{"buffer":{"length":1}}},{"name":"hashbytes","type":{"buffer":{"length":32}}}],"outputs":{"type":"bool"}} as TypedAbiFunction<[version: TypedAbiArg, hashbytes: TypedAbiArg], boolean>, - checkPoxAddrVersion: {"name":"check-pox-addr-version","access":"read_only","args":[{"name":"version","type":{"buffer":{"length":1}}}],"outputs":{"type":"bool"}} as TypedAbiFunction<[version: TypedAbiArg], boolean>, + checkCallerAllowed: {"name":"check-caller-allowed","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[], Response>, checkPoxLockPeriod: {"name":"check-pox-lock-period","access":"read_only","args":[{"name":"lock-period","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[lockPeriod: TypedAbiArg], boolean>, + constructLockupOutputScript: {"name":"construct-lockup-output-script","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"buffer":{"length":34}}}} as TypedAbiFunction<[staker: TypedAbiArg, unlockBurnHeight: TypedAbiArg, unlockBytes: TypedAbiArg, earlyUnlockBytes: TypedAbiArg], Uint8Array>, + constructLockupScript: {"name":"construct-lockup-script","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"buffer":{"length":5141}}}} as TypedAbiFunction<[staker: TypedAbiArg, unlockBurnHeight: TypedAbiArg, unlockBytes: TypedAbiArg, earlyUnlockBytes: TypedAbiArg], Uint8Array>, + currentDistributionCycle: {"name":"current-distribution-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, currentPoxRewardCycle: {"name":"current-pox-reward-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, - getPoolInfo: {"name":"get-pool-info","access":"read_only","args":[{"name":"owner","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}} as TypedAbiFunction<[owner: TypedAbiArg], { - "poxAddr": { - "hashbytes": Uint8Array; - "version": Uint8Array; -}; - "signerKey": Uint8Array; + distributionCycleToBurnHeight: {"name":"distribution-cycle-to-burn-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, + getAmountDelegatedForSigner: {"name":"get-amount-delegated-for-signer","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], bigint>, + getBcHHash: {"name":"get-bc-h-hash","access":"read_only","args":[{"name":"bh","type":"uint128"}],"outputs":{"type":{"optional":{"buffer":{"length":32}}}}} as TypedAbiFunction<[bh: TypedAbiArg], Uint8Array | null>, + getBondAllowance: {"name":"get-bond-allowance","access":"read_only","args":[{"name":"bond-index","type":"uint128"},{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":"uint128"}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, staker: TypedAbiArg], bigint | null>, + getBondL1UnlockHeight: {"name":"get-bond-l1-unlock-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + getBondMembership: {"name":"get-bond-membership","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"bond-index","type":"uint128"},{"name":"is-l1-lock","type":"bool"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { + "amountUstx": bigint; + "bondIndex": bigint; + "isL1Lock": boolean; + "signer": string; } | null>, + getEarned: {"name":"get-earned","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getLastAccountedRewardsOnly: {"name":"get-last-accounted-rewards-only","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getLastRewardComputeHeight: {"name":"get-last-reward-compute-height","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getNewRewards: {"name":"get-new-rewards","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, getPoxInfo: {"name":"get-pox-info","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-burnchain-block-height","type":"uint128"},{"name":"min-amount-ustx","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-id","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"total-liquid-supply-ustx","type":"uint128"}]},"error":"none"}}}} as TypedAbiFunction<[], Response<{ "firstBurnchainBlockHeight": bigint; "minAmountUstx": bigint; @@ -224,26 +261,33 @@ export const contracts = { "rewardCycleLength": bigint; "totalLiquidSupplyUstx": bigint; }, null>>, - getSignerGrantMessageHash: {"name":"get-signer-grant-message-hash","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"pox-addr","type":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[staker: TypedAbiArg, poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -} | null, "poxAddr">, authId: TypedAbiArg], Uint8Array>, - getSignerKeyMessageHash: {"name":"get-signer-key-message-hash","access":"read_only","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Uint8Array>, - getStakerInfo: {"name":"get-staker-info","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { + getProtocolBond: {"name":"get-protocol-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"early-unlock-admin","type":"principal"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg], { + "earlyUnlockAdmin": string; + "earlyUnlockSigners": Uint8Array; + "minUstxRatio": bigint; + "stxValueRatio": bigint; + "targetRate": bigint; +} | null>, + getReserveBalance: {"name":"get-reserve-balance","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getReversedTxid: {"name":"get-reversed-txid","access":"read_only","args":[{"name":"tx","type":{"buffer":{"length":100000}}}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[tx: TypedAbiArg], Uint8Array>, + getRewards: {"name":"get-rewards","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getSignerCycleMembership: {"name":"get-signer-cycle-membership","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], { + "amountUstx": bigint; + "signer": string; +} | null>, + getSignerGrantMessageHash: {"name":"get-signer-grant-message-hash","access":"read_only","args":[{"name":"signer-manager","type":"principal"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, authId: TypedAbiArg], Uint8Array>, + getSignerInfo: {"name":"get-signer-info","access":"read_only","args":[{"name":"signer","type":"principal"}],"outputs":{"type":{"optional":{"buffer":{"length":33}}}}} as TypedAbiFunction<[signer: TypedAbiArg], Uint8Array | null>, + getSignerKey: {"name":"get-signer-key","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"buffer":{"length":33}}}}} as TypedAbiFunction<[staker: TypedAbiArg], Uint8Array | null>, + getSignerPendingRewardsForCycle: {"name":"get-signer-pending-rewards-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getSignerPendingStakedUstxPerCycle: {"name":"get-signer-pending-staked-ustx-per-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], bigint>, + getSignerRewardsPerTokenPaidForCycle: {"name":"get-signer-rewards-per-token-paid-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getSignerSharesStakedForCycle: {"name":"get-signer-shares-staked-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getStakerInfo: {"name":"get-staker-info","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { "amountUstx": bigint; "firstRewardCycle": bigint; "numCycles": bigint; - "poolOrSoloInfo": Response; - "unlockBytes": Uint8Array; + "signer": string; } | null>, getStakerSetFirstItemForCycle: {"name":"get-staker-set-first-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, getStakerSetItemForCycle: {"name":"get-staker-set-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], { @@ -253,33 +297,114 @@ export const contracts = { getStakerSetLastItemForCycle: {"name":"get-staker-set-last-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, getStakerSetNextItemForCycle: {"name":"get-staker-set-next-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], string | null>, getStakerSetPrevItemForCycle: {"name":"get-staker-set-prev-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], string | null>, + getStakerSharesStakedForCycle: {"name":"get-staker-shares-staked-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg, signer: TypedAbiArg], bigint>, + getTotalSbtcStaked: {"name":"get-total-sbtc-staked","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + getTotalSbtcStakedForBond: {"name":"get-total-sbtc-staked-for-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, + getTotalSharesStakedForCycle: {"name":"get-total-shares-staked-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getUstxDelegatedForCycle: {"name":"get-ustx-delegated-for-cycle","access":"read_only","args":[{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[rewardCycle: TypedAbiArg], bigint>, + isBondActiveAtHeight: {"name":"is-bond-active-at-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"},{"name":"calculation-height","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[bondIndex: TypedAbiArg, calculationHeight: TypedAbiArg], boolean>, + isInPreparePhase: {"name":"is-in-prepare-phase","access":"read_only","args":[{"name":"current-cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[currentCycle: TypedAbiArg], boolean>, + minUstxForSatsAmount: {"name":"min-ustx-for-sats-amount","access":"read_only","args":[{"name":"sats-amount","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[satsAmount: TypedAbiArg, stxValueRatio: TypedAbiArg, minUstxRatio: TypedAbiArg], bigint>, + parseBlockHeader: {"name":"parse-block-header","access":"read_only","args":[{"name":"headerbuff","type":{"buffer":{"length":80}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"merkle-root","type":{"buffer":{"length":32}}},{"name":"nbits","type":"uint128"},{"name":"nonce","type":"uint128"},{"name":"parent","type":{"buffer":{"length":32}}},{"name":"timestamp","type":"uint128"},{"name":"version","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[headerbuff: TypedAbiArg], Response<{ + "merkleRoot": Uint8Array; + "nbits": bigint; + "nonce": bigint; + "parent": Uint8Array; + "timestamp": bigint; + "version": bigint; +}, bigint>>, + pushCScriptNum: {"name":"push-c-script-num","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":1027}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, + pushScriptBytes: {"name":"push-script-bytes","access":"read_only","args":[{"name":"bytes","type":{"buffer":{"length":1024}}}],"outputs":{"type":{"buffer":{"length":1027}}}} as TypedAbiFunction<[bytes: TypedAbiArg], Uint8Array>, + readHashslice: {"name":"read-hashslice","access":"read_only","args":[{"name":"old-ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}},{"name":"hashslice","type":{"buffer":{"length":32}}}]},"error":"uint128"}}}} as TypedAbiFunction<[oldCtx: TypedAbiArg<{ + "index": number | bigint; + "txbuff": Uint8Array; +}, "oldCtx">], Response<{ + "ctx": { + "index": bigint; + "txbuff": Uint8Array; +}; + "hashslice": Uint8Array; +}, bigint>>, + readUint32: {"name":"read-uint32","access":"read_only","args":[{"name":"ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"ctx","type":{"tuple":[{"name":"index","type":"uint128"},{"name":"txbuff","type":{"buffer":{"length":4096}}}]}},{"name":"uint32","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[ctx: TypedAbiArg<{ + "index": number | bigint; + "txbuff": Uint8Array; +}, "ctx">], Response<{ + "ctx": { + "index": bigint; + "txbuff": Uint8Array; +}; + "uint32": bigint; +}, bigint>>, + reverseBuff32: {"name":"reverse-buff32","access":"read_only","args":[{"name":"input","type":{"buffer":{"length":32}}}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[input: TypedAbiArg], Uint8Array>, rewardCycleToBurnHeight: {"name":"reward-cycle-to-burn-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, rewardCycleToUnlockHeight: {"name":"reward-cycle-to-unlock-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, + serializeCScriptNum: {"name":"serialize-c-script-num","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":5}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, stakerSetContainsForCycle: {"name":"staker-set-contains-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], boolean>, - verifySignerKeyGrant: {"name":"verify-signer-key-grant","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, signerKey: TypedAbiArg, poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">], Response>, - verifySignerKeySig: {"name":"verify-signer-key-sig","access":"read_only","args":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"topic","type":{"string-ascii":{"length":14}}},{"name":"period","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"amount","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[poxAddr: TypedAbiArg<{ - "hashbytes": Uint8Array; - "version": Uint8Array; -}, "poxAddr">, rewardCycle: TypedAbiArg, topic: TypedAbiArg, period: TypedAbiArg, signerSig: TypedAbiArg, signerKey: TypedAbiArg, amount: TypedAbiArg, maxAmount: TypedAbiArg, authId: TypedAbiArg], Response> + uintToBuffLe: {"name":"uint-to-buff-le","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":2}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, + verifyBlockHeader: {"name":"verify-block-header","access":"read_only","args":[{"name":"headerbuff","type":{"buffer":{"length":80}}},{"name":"expected-block-height","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[headerbuff: TypedAbiArg, expectedBlockHeight: TypedAbiArg], boolean>, + verifySignerKeyGrant: {"name":"verify-signer-key-grant","access":"read_only","args":[{"name":"signer-manager","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg], Response> }, "maps": { - pools: {"name":"pools","key":"principal","value":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}} as TypedAbiMap, + protocolBondAllowances: {"name":"protocol-bond-allowances","key":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "bondIndex": number | bigint; + "staker": string; +}, bigint>, + protocolBondMemberships: {"name":"protocol-bond-memberships","key":"principal","value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"bond-index","type":"uint128"},{"name":"is-l1-lock","type":"bool"},{"name":"signer","type":"principal"}]}} as TypedAbiMap, - signerKeyGrants: {"name":"signer-key-grants","key":{"tuple":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"staker","type":"principal"}]},"value":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}} as TypedAbiMap<{ + protocolBonds: {"name":"protocol-bonds","key":"uint128","value":{"tuple":[{"name":"early-unlock-admin","type":"principal"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]}} as TypedAbiMap, + protocolBondsTotalStaked: {"name":"protocol-bonds-total-staked","key":"uint128","value":"uint128"} as TypedAbiMap, + rewardsPerTokenForCycle: {"name":"rewards-per-token-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; +}, bigint>, + signerDelegatedPerCycle: {"name":"signer-delegated-per-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "cycle": number | bigint; + "signer": string; +}, bigint>, + signerKeyGrants: {"name":"signer-key-grants","key":{"tuple":[{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-manager","type":"principal"}]},"value":"bool"} as TypedAbiMap<{ "signerKey": Uint8Array; - "staker": string; -}, { - "hashbytes": Uint8Array; - "version": Uint8Array; -} | null>, + "signerManager": string; +}, boolean>, + signerPendingRewardsForCycle: {"name":"signer-pending-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; +}, bigint>, + signerPendingStakedUstxPerCycle: {"name":"signer-pending-staked-ustx-per-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "cycle": number | bigint; + "signer": string; +}, bigint>, + signerRewardsPerTokenPaidForCycle: {"name":"signer-rewards-per-token-paid-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; +}, bigint>, + signerSharesStakedForCycle: {"name":"signer-shares-staked-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; +}, bigint>, + signers: {"name":"signers","key":"principal","value":{"buffer":{"length":33}}} as TypedAbiMap, + stakerInfo: {"name":"staker-info","key":"principal","value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"signer","type":"principal"}]}} as TypedAbiMap, stakerSetLlFirstForCycle: {"name":"staker-set-ll-first-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, stakerSetLlForCycle: {"name":"staker-set-ll-for-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"value":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}} as TypedAbiMap<{ "cycle": number | bigint; @@ -289,38 +414,73 @@ export const contracts = { "prev": string | null; }>, stakerSetLlLastForCycle: {"name":"staker-set-ll-last-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, - stakingState: {"name":"staking-state","key":"principal","value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"pool-or-solo-info","type":{"response":{"ok":"principal","error":{"tuple":[{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"signer-key","type":{"buffer":{"length":33}}}]}}}},{"name":"unlock-bytes","type":{"buffer":{"length":683}}}]}} as TypedAbiMap, + stakerSignerCycleMemberships: {"name":"staker-signer-cycle-memberships","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"signer","type":"principal"}]}} as TypedAbiMap<{ + "cycle": number | bigint; + "staker": string; +}, { "amountUstx": bigint; - "firstRewardCycle": bigint; - "numCycles": bigint; - "poolOrSoloInfo": Response; - "unlockBytes": Uint8Array; + "signer": string; }>, - usedSignerKeyAuthorizations: {"name":"used-signer-key-authorizations","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"period","type":"uint128"},{"name":"pox-addr","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"reward-cycle","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"topic","type":{"string-ascii":{"length":14}}}]},"value":"bool"} as TypedAbiMap<{ + totalSharesStakedForCycle: {"name":"total-shares-staked-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; +}, bigint>, + usedSignerKeyAuthorizations: {"name":"used-signer-key-authorizations","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"period","type":"uint128"},{"name":"pox-addr","type":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}},{"name":"reward-cycle","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"topic","type":{"string-ascii":{"length":14}}}]},"value":"bool"} as TypedAbiMap<{ "authId": number | bigint; "maxAmount": number | bigint; "period": number | bigint; "poxAddr": { "hashbytes": Uint8Array; "version": Uint8Array; -}; +} | null; "rewardCycle": number | bigint; "signerKey": Uint8Array; "topic": string; }, boolean>, - usedSignerKeyGrants: {"name":"used-signer-key-grants","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"staker","type":"principal"}]},"value":"bool"} as TypedAbiMap<{ + usedSignerKeyGrants: {"name":"used-signer-key-grants","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-manager","type":"principal"}]},"value":"bool"} as TypedAbiMap<{ "authId": number | bigint; "signerKey": Uint8Array; - "staker": string; -}, boolean> + "signerManager": string; +}, boolean>, + ustxDelegatedPerCycle: {"name":"ustx-delegated-per-cycle","key":"uint128","value":"uint128"} as TypedAbiMap }, "variables": { + BOND_GAP_CYCLES: { + name: 'BOND_GAP_CYCLES', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + BOND_LENGTH_CYCLES: { + name: 'BOND_LENGTH_CYCLES', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + ERR_ACTIVE_BOND_NOT_INCLUDED: { + name: 'ERR_ACTIVE_BOND_NOT_INCLUDED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_ALREADY_REGISTERED: { + name: 'ERR_ALREADY_REGISTERED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, ERR_ALREADY_STAKED: { name: 'ERR_ALREADY_STAKED', type: { @@ -331,8 +491,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_CANNOT_EXTEND: { - name: 'ERR_CANNOT_EXTEND', + ERR_BOND_ALREADY_SETUP: { + name: 'ERR_BOND_ALREADY_SETUP', type: { response: { ok: 'none', @@ -341,8 +501,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INSUFFICIENT_FUNDS: { - name: 'ERR_INSUFFICIENT_FUNDS', + ERR_BOND_ALREADY_STARTED: { + name: 'ERR_BOND_ALREADY_STARTED', type: { response: { ok: 'none', @@ -351,8 +511,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_AMOUNT: { - name: 'ERR_INVALID_AMOUNT', + ERR_BOND_NOT_ACTIVE: { + name: 'ERR_BOND_NOT_ACTIVE', type: { response: { ok: 'none', @@ -361,8 +521,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_NUM_CYCLES: { - name: 'ERR_INVALID_NUM_CYCLES', + ERR_BOND_NOT_FOUND: { + name: 'ERR_BOND_NOT_FOUND', type: { response: { ok: 'none', @@ -371,8 +531,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_POX_ADDRESS: { - name: 'ERR_INVALID_POX_ADDRESS', + eRR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK: { + name: 'ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK', type: { response: { ok: 'none', @@ -381,8 +541,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_SIGNATURE_PUBKEY: { - name: 'ERR_INVALID_SIGNATURE_PUBKEY', + ERR_CANNOT_SETUP_BOND_TOO_LATE: { + name: 'ERR_CANNOT_SETUP_BOND_TOO_LATE', type: { response: { ok: 'none', @@ -391,8 +551,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_SIGNATURE_RECOVER: { - name: 'ERR_INVALID_SIGNATURE_RECOVER', + ERR_CANNOT_SETUP_BOND_TOO_SOON: { + name: 'ERR_CANNOT_SETUP_BOND_TOO_SOON', type: { response: { ok: 'none', @@ -401,8 +561,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_START_BURN_HEIGHT: { - name: 'ERR_INVALID_START_BURN_HEIGHT', + ERR_CANNOT_UNSTAKE_SBTC: { + name: 'ERR_CANNOT_UNSTAKE_SBTC', type: { response: { ok: 'none', @@ -411,8 +571,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_NOT_ALLOWED: { - name: 'ERR_NOT_ALLOWED', + ERR_DISTRIBUTION_ALREADY_COMPUTED: { + name: 'ERR_DISTRIBUTION_ALREADY_COMPUTED', type: { response: { ok: 'none', @@ -421,8 +581,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_NOT_STAKED: { - name: 'ERR_NOT_STAKED', + ERR_INSUFFICIENT_STX: { + name: 'ERR_INSUFFICIENT_STX', type: { response: { ok: 'none', @@ -431,8 +591,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_POOL_NOT_FOUND: { - name: 'ERR_POOL_NOT_FOUND', + ERR_INVALID_BOND_PERIOD_ORDERING: { + name: 'ERR_INVALID_BOND_PERIOD_ORDERING', type: { response: { ok: 'none', @@ -441,8 +601,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { - name: 'ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH', + ERR_INVALID_BTC_HEADER: { + name: 'ERR_INVALID_BTC_HEADER', type: { response: { ok: 'none', @@ -451,8 +611,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_SIGNER_AUTH_USED: { - name: 'ERR_SIGNER_AUTH_USED', + ERR_INVALID_LOCKUP_SCRIPT: { + name: 'ERR_INVALID_LOCKUP_SCRIPT', type: { response: { ok: 'none', @@ -461,8 +621,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_SIGNER_KEY_GRANT_NOT_FOUND: { - name: 'ERR_SIGNER_KEY_GRANT_NOT_FOUND', + ERR_INVALID_MERKLE_PROOF: { + name: 'ERR_INVALID_MERKLE_PROOF', type: { response: { ok: 'none', @@ -471,8 +631,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { - name: 'ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH', + ERR_INVALID_NUM_CYCLES: { + name: 'ERR_INVALID_NUM_CYCLES', type: { response: { ok: 'none', @@ -481,8 +641,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_SIGNER_KEY_GRANT_USED: { - name: 'ERR_SIGNER_KEY_GRANT_USED', + ERR_INVALID_OLD_SIGNER_MANAGER: { + name: 'ERR_INVALID_OLD_SIGNER_MANAGER', type: { response: { ok: 'none', @@ -491,8 +651,258 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - MAX_ADDRESS_VERSION: { - name: 'MAX_ADDRESS_VERSION', + ERR_INVALID_POX_ADDRESS: { + name: 'ERR_INVALID_POX_ADDRESS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_SIGNATURE_PUBKEY: { + name: 'ERR_INVALID_SIGNATURE_PUBKEY', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_SIGNATURE_RECOVER: { + name: 'ERR_INVALID_SIGNATURE_RECOVER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_START_BURN_HEIGHT: { + name: 'ERR_INVALID_START_BURN_HEIGHT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_UNSTAKE_SBTC_AMOUNT: { + name: 'ERR_INVALID_UNSTAKE_SBTC_AMOUNT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + eRR_L1_LOCKUP_NOT_FOUND: { + name: 'ERR_L1_LOCKUP_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_ALLOWLISTED: { + name: 'ERR_NOT_ALLOWLISTED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_BOND_PARTICIPANT: { + name: 'ERR_NOT_BOND_PARTICIPANT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NOT_STAKING: { + name: 'ERR_NOT_STAKING', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NO_CLAIMABLE_REWARDS: { + name: 'ERR_NO_CLAIMABLE_REWARDS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_NO_SBTC_BALANCE: { + name: 'ERR_NO_SBTC_BALANCE', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_READ_TX_OUT_OF_BOUNDS: { + name: 'ERR_READ_TX_OUT_OF_BOUNDS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { + name: 'ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_AUTH_USED: { + name: 'ERR_SIGNER_AUTH_USED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_NOT_FOUND: { + name: 'ERR_SIGNER_KEY_GRANT_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { + name: 'ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_KEY_GRANT_USED: { + name: 'ERR_SIGNER_KEY_GRANT_USED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_SIGNER_NOT_FOUND: { + name: 'ERR_SIGNER_NOT_FOUND', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_STAKER_ALREADY_ADDED: { + name: 'ERR_STAKER_ALREADY_ADDED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_TOO_MUCH_SATS: { + name: 'ERR_TOO_MUCH_SATS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED: { + name: 'ERR_UNAUTHORIZED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED_CALLER: { + name: 'ERR_UNAUTHORIZED_CALLER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED_SIGNER_REGISTRATION: { + name: 'ERR_UNAUTHORIZED_SIGNER_REGISTRATION', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNSTAKE_IN_PREPARE_PHASE: { + name: 'ERR_UNSTAKE_IN_PREPARE_PHASE', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UPDATE_BOND_SAME_SIGNER: { + name: 'ERR_UPDATE_BOND_SAME_SIGNER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + MAX_ADDRESS_VERSION: { + name: 'MAX_ADDRESS_VERSION', type: 'uint128', access: 'constant' } as TypedAbiVariable, @@ -510,11 +920,6 @@ export const contracts = { name: 'MAX_NUM_CYCLES', type: 'uint128', access: 'constant' -} as TypedAbiVariable, - MIN_STACKING_AMOUNT: { - name: 'MIN_STACKING_AMOUNT', - type: 'uint128', - access: 'constant' } as TypedAbiVariable, pOX_5_SIGNER_DOMAIN: { name: 'POX_5_SIGNER_DOMAIN', @@ -548,13 +953,18 @@ export const contracts = { "name": string; "version": string; }>, - PREPARE_CYCLE_LENGTH: { - name: 'PREPARE_CYCLE_LENGTH', + PRECISION: { + name: 'PRECISION', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + RESERVE_RATIO: { + name: 'RESERVE_RATIO', type: 'uint128', access: 'constant' } as TypedAbiVariable, - REWARD_CYCLE_LENGTH: { - name: 'REWARD_CYCLE_LENGTH', + SIGNER_SET_MIN_USTX: { + name: 'SIGNER_SET_MIN_USTX', type: 'uint128', access: 'constant' } as TypedAbiVariable, @@ -585,11 +995,21 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable, + bondAdmin: { + name: 'bond-admin', + type: 'principal', + access: 'variable' +} as TypedAbiVariable, configured: { name: 'configured', type: 'bool', access: 'variable' } as TypedAbiVariable, + firstBondPeriodCycle: { + name: 'first-bond-period-cycle', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, firstBurnchainBlockHeight: { name: 'first-burnchain-block-height', type: 'uint128', @@ -599,6 +1019,16 @@ export const contracts = { name: 'first-pox-5-reward-cycle', type: 'uint128', access: 'variable' +} as TypedAbiVariable, + lastAccountedRewardsOnly: { + name: 'last-accounted-rewards-only', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + lastRewardComputeHeight: { + name: 'last-reward-compute-height', + type: 'uint128', + access: 'variable' } as TypedAbiVariable, poxPrepareCycleLength: { name: 'pox-prepare-cycle-length', @@ -609,109 +1039,794 @@ export const contracts = { name: 'pox-reward-cycle-length', type: 'uint128', access: 'variable' +} as TypedAbiVariable, + reserveBalance: { + name: 'reserve-balance', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + totalSbtcStaked: { + name: 'total-sbtc-staked', + type: 'uint128', + access: 'variable' } as TypedAbiVariable }, constants: { - ERR_ALREADY_STAKED: { + BOND_GAP_CYCLES: 2n, + BOND_LENGTH_CYCLES: 12n, + ERR_ACTIVE_BOND_NOT_INCLUDED: { isOk: false, - value: 1n + value: 33n }, - ERR_CANNOT_EXTEND: { + ERR_ALREADY_REGISTERED: { isOk: false, - value: 10n + value: 9n + }, + ERR_ALREADY_STAKED: { + isOk: false, + value: 19n }, - ERR_INSUFFICIENT_FUNDS: { + ERR_BOND_ALREADY_SETUP: { isOk: false, value: 4n }, - ERR_INVALID_AMOUNT: { + ERR_BOND_ALREADY_STARTED: { isOk: false, - value: 11n + value: 43n }, - ERR_INVALID_NUM_CYCLES: { + ERR_BOND_NOT_ACTIVE: { isOk: false, - value: 9n + value: 31n }, - ERR_INVALID_POX_ADDRESS: { + ERR_BOND_NOT_FOUND: { isOk: false, - value: 13n + value: 7n }, - ERR_INVALID_SIGNATURE_PUBKEY: { + eRR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK: { isOk: false, - value: 17n + value: 35n }, - ERR_INVALID_SIGNATURE_RECOVER: { + ERR_CANNOT_SETUP_BOND_TOO_LATE: { isOk: false, - value: 16n + value: 3n }, - ERR_INVALID_START_BURN_HEIGHT: { + ERR_CANNOT_SETUP_BOND_TOO_SOON: { + isOk: false, + value: 2n + }, + ERR_CANNOT_UNSTAKE_SBTC: { + isOk: false, + value: 38n + }, + ERR_DISTRIBUTION_ALREADY_COMPUTED: { + isOk: false, + value: 30n + }, + ERR_INSUFFICIENT_STX: { isOk: false, value: 8n }, - ERR_NOT_ALLOWED: { + ERR_INVALID_BOND_PERIOD_ORDERING: { isOk: false, - value: 23n + value: 29n }, - ERR_NOT_STAKED: { + ERR_INVALID_BTC_HEADER: { isOk: false, - value: 2n + value: 40n + }, + ERR_INVALID_LOCKUP_SCRIPT: { + isOk: false, + value: 42n }, - ERR_POOL_NOT_FOUND: { + ERR_INVALID_MERKLE_PROOF: { + isOk: false, + value: 41n + }, + ERR_INVALID_NUM_CYCLES: { + isOk: false, + value: 20n + }, + ERR_INVALID_OLD_SIGNER_MANAGER: { + isOk: false, + value: 36n + }, + ERR_INVALID_POX_ADDRESS: { + isOk: false, + value: 21n + }, + ERR_INVALID_SIGNATURE_PUBKEY: { isOk: false, value: 14n }, + ERR_INVALID_SIGNATURE_RECOVER: { + isOk: false, + value: 13n + }, + ERR_INVALID_START_BURN_HEIGHT: { + isOk: false, + value: 24n + }, + ERR_INVALID_UNSTAKE_SBTC_AMOUNT: { + isOk: false, + value: 37n + }, + eRR_L1_LOCKUP_NOT_FOUND: { + isOk: false, + value: 6n + }, + ERR_NOT_ALLOWLISTED: { + isOk: false, + value: 11n + }, + ERR_NOT_BOND_PARTICIPANT: { + isOk: false, + value: 34n + }, + ERR_NOT_STAKING: { + isOk: false, + value: 27n + }, + ERR_NO_CLAIMABLE_REWARDS: { + isOk: false, + value: 32n + }, + ERR_NO_SBTC_BALANCE: { + isOk: false, + value: 25n + }, + ERR_READ_TX_OUT_OF_BOUNDS: { + isOk: false, + value: 39n + }, ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { isOk: false, - value: 19n + value: 15n }, ERR_SIGNER_AUTH_USED: { isOk: false, - value: 20n + value: 16n }, ERR_SIGNER_KEY_GRANT_NOT_FOUND: { isOk: false, - value: 21n + value: 17n }, ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { isOk: false, - value: 22n + value: 18n }, ERR_SIGNER_KEY_GRANT_USED: { isOk: false, - value: 15n + value: 12n + }, + ERR_SIGNER_NOT_FOUND: { + isOk: false, + value: 23n + }, + ERR_STAKER_ALREADY_ADDED: { + isOk: false, + value: 5n + }, + ERR_TOO_MUCH_SATS: { + isOk: false, + value: 10n + }, + ERR_UNAUTHORIZED: { + isOk: false, + value: 1n + }, + ERR_UNAUTHORIZED_CALLER: { + isOk: false, + value: 22n + }, + ERR_UNAUTHORIZED_SIGNER_REGISTRATION: { + isOk: false, + value: 26n + }, + ERR_UNSTAKE_IN_PREPARE_PHASE: { + isOk: false, + value: 28n + }, + ERR_UPDATE_BOND_SAME_SIGNER: { + isOk: false, + value: 44n }, MAX_ADDRESS_VERSION: 6n, mAX_ADDRESS_VERSION_BUFF_20: 4n, mAX_ADDRESS_VERSION_BUFF_32: 6n, - MAX_NUM_CYCLES: 24n, - MIN_STACKING_AMOUNT: 100_000_000n, + MAX_NUM_CYCLES: 96n, pOX_5_SIGNER_DOMAIN: { chainId: 2_147_483_648n, name: 'pox-5-signer', version: '1.0.0' }, - PREPARE_CYCLE_LENGTH: 50n, - REWARD_CYCLE_LENGTH: 1_050n, + PRECISION: 1_000_000_000_000_000_000n, + RESERVE_RATIO: 1_500n, + SIGNER_SET_MIN_USTX: 50_000_000_000n, sIP018_MSG_PREFIX: Uint8Array.from([83,73,80,48,49,56]), STACKS_ADDR_VERSION_MAINNET: Uint8Array.from([22]), STACKS_ADDR_VERSION_TESTNET: Uint8Array.from([26]), + bondAdmin: 'SP000000000000000000002Q6VF78', configured: false, + firstBondPeriodCycle: 0n, firstBurnchainBlockHeight: 0n, firstPox5RewardCycle: 0n, + lastAccountedRewardsOnly: 0n, + lastRewardComputeHeight: 0n, poxPrepareCycleLength: 50n, - poxRewardCycleLength: 1_050n + poxRewardCycleLength: 1_050n, + reserveBalance: 0n, + totalSbtcStaked: 0n }, "non_fungible_tokens": [ ], "fungible_tokens":[],"epoch":"Epoch33","clarity_version":"Clarity4", contractName: 'pox-5', + }, +pox5Signer: { + "functions": { + checkpointStakerForIndex: {"name":"checkpoint-staker-for-index","access":"private","args":[{"name":"index-offset","type":"uint128"},{"name":"acc-res","type":{"response":{"ok":{"tuple":[{"name":"first-index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[indexOffset: TypedAbiArg, accRes: TypedAbiArg, "accRes">], Response<{ + "firstIndex": bigint; + "isBond": boolean; + "staker": string; +}, bigint>>, + crystallizeStakerRewards: {"name":"crystallize-staker-rewards","access":"private","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, + updateBondRewardsInfo: {"name":"update-bond-rewards-info","access":"private","args":[{"name":"bond-info","type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}},{"name":"acc","type":"bool"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[bondInfo: TypedAbiArg<{ + "bondIndex": number | bigint; + "earned": number | bigint; + "rewardsPerToken": number | bigint; +}, "bondInfo">, acc: TypedAbiArg], boolean>, + updateRewardsInfo: {"name":"update-rewards-info","access":"private","args":[{"name":"rewards-per-share","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[rewardsPerShare: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], boolean>, + checkpointStaker: {"name":"checkpoint-staker","access":"public","args":[{"name":"staker","type":"principal"},{"name":"first-index","type":"uint128"},{"name":"num-indexes","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstIndex: TypedAbiArg, numIndexes: TypedAbiArg, isBond: TypedAbiArg], Response>, + claimRewards: {"name":"claim-rewards","access":"public","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}},{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"bond-totals","type":"uint128"},{"name":"stx-rewards","type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}},{"name":"total-rewards","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg, rewardCycle: TypedAbiArg], Response<{ + "bondRewards": { + "bondIndex": bigint; + "earned": bigint; + "rewardsPerToken": bigint; +}[]; + "bondTotals": bigint; + "stxRewards": { + "earned": bigint; + "rewardsPerToken": bigint; +}; + "totalRewards": bigint; +}, bigint>>, + claimStakerRewards: {"name":"claim-staker-rewards","access":"public","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], Response>, + registerSelf: {"name":"register-self","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"auth-id","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"signer","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg, authId: TypedAbiArg, signerSig: TypedAbiArg], Response<{ + "signer": string; + "signerKey": Uint8Array; +}, bigint>>, + updateAllowedCaller: {"name":"update-allowed-caller","access":"public","args":[{"name":"new-allowed-caller","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[newAllowedCaller: TypedAbiArg], Response>, + validateStake_x: {"name":"validate-stake!","access":"public","args":[{"name":"staker","type":"principal"},{"name":"first-index","type":"uint128"},{"name":"num-indexes","type":"uint128"},{"name":"amount-ustx","type":"uint128"},{"name":"amount-sats","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstIndex: TypedAbiArg, numIndexes: TypedAbiArg, amountUstx: TypedAbiArg, amountSats: TypedAbiArg, isBond: TypedAbiArg, signerCalldata: TypedAbiArg], Response>, + getEarnedStakerRewards: {"name":"get-earned-staker-rewards","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getStakerPendingRewardsForCycle: {"name":"get-staker-pending-rewards-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getStakerRewardsPerTokenPaidForCycle: {"name":"get-staker-rewards-per-token-paid-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint> + }, + "maps": { + rewardsPerTokenForCycle: {"name":"rewards-per-token-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; +}, bigint>, + stakerPendingRewardsForCycle: {"name":"staker-pending-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "staker": string; +}, bigint>, + stakerRewardsPaidPerTokenForCycle: {"name":"staker-rewards-paid-per-token-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "staker": string; +}, bigint> + }, + "variables": { + ERR_NO_CLAIMABLE_REWARDS: { + name: 'ERR_NO_CLAIMABLE_REWARDS', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + PRECISION: { + name: 'PRECISION', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + allowedCaller: { + name: 'allowed-caller', + type: 'principal', + access: 'variable' +} as TypedAbiVariable + }, + constants: { + ERR_NO_CLAIMABLE_REWARDS: { + isOk: false, + value: 1_001n + }, + PRECISION: 1_000_000_000_000_000_000n, + allowedCaller: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM' +}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch33","clarity_version":"Clarity4", + contractName: 'pox-5-signer', + }, +sbtcRegistry: { + "functions": { + incrementLastWithdrawalRequestId: {"name":"increment-last-withdrawal-request-id","access":"private","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, + completeDeposit: {"name":"complete-deposit","access":"public","args":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"},{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[txid: TypedAbiArg, voutIndex: TypedAbiArg, amount: TypedAbiArg, recipient: TypedAbiArg, burnHash: TypedAbiArg, burnHeight: TypedAbiArg, sweepTxid: TypedAbiArg], Response>, + completeWithdrawalAccept: {"name":"complete-withdrawal-accept","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"bitcoin-txid","type":{"buffer":{"length":32}}},{"name":"output-index","type":"uint128"},{"name":"signer-bitmap","type":"uint128"},{"name":"fee","type":"uint128"},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, bitcoinTxid: TypedAbiArg, outputIndex: TypedAbiArg, signerBitmap: TypedAbiArg, fee: TypedAbiArg, burnHash: TypedAbiArg, burnHeight: TypedAbiArg, sweepTxid: TypedAbiArg], Response>, + completeWithdrawalReject: {"name":"complete-withdrawal-reject","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, signerBitmap: TypedAbiArg], Response>, + createWithdrawalRequest: {"name":"create-withdrawal-request","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"max-fee","type":"uint128"},{"name":"sender","type":"principal"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"height","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, maxFee: TypedAbiArg, sender: TypedAbiArg, recipient: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "recipient">, height: TypedAbiArg], Response>, + rotateKeys: {"name":"rotate-keys","access":"public","args":[{"name":"new-keys","type":{"list":{"type":{"buffer":{"length":33}},"length":128}}},{"name":"new-address","type":"principal"},{"name":"new-aggregate-pubkey","type":{"buffer":{"length":33}}},{"name":"new-signature-threshold","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newKeys: TypedAbiArg, newAddress: TypedAbiArg, newAggregatePubkey: TypedAbiArg, newSignatureThreshold: TypedAbiArg], Response>, + updateProtocolContract: {"name":"update-protocol-contract","access":"public","args":[{"name":"contract-type","type":{"buffer":{"length":1}}},{"name":"new-contract","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[contractType: TypedAbiArg, newContract: TypedAbiArg], Response>, + getActiveProtocol: {"name":"get-active-protocol","access":"read_only","args":[{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[contractFlag: TypedAbiArg], string | null>, + getCompletedDeposit: {"name":"get-completed-deposit","access":"read_only","args":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}}}} as TypedAbiFunction<[txid: TypedAbiArg, voutIndex: TypedAbiArg], { + "amount": bigint; + "recipient": string; + "sweepBurnHash": Uint8Array; + "sweepBurnHeight": bigint; + "sweepTxid": Uint8Array; +} | null>, + getCompletedWithdrawalSweepData: {"name":"get-completed-withdrawal-sweep-data","access":"read_only","args":[{"name":"id","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}}}} as TypedAbiFunction<[id: TypedAbiArg], { + "sweepBurnHash": Uint8Array; + "sweepBurnHeight": bigint; + "sweepTxid": Uint8Array; +} | null>, + getCurrentAggregatePubkey: {"name":"get-current-aggregate-pubkey","access":"read_only","args":[],"outputs":{"type":{"buffer":{"length":33}}}} as TypedAbiFunction<[], Uint8Array>, + getCurrentSignerData: {"name":"get-current-signer-data","access":"read_only","args":[],"outputs":{"type":{"tuple":[{"name":"current-aggregate-pubkey","type":{"buffer":{"length":33}}},{"name":"current-signature-threshold","type":"uint128"},{"name":"current-signer-principal","type":"principal"},{"name":"current-signer-set","type":{"list":{"type":{"buffer":{"length":33}},"length":128}}}]}}} as TypedAbiFunction<[], { + "currentAggregatePubkey": Uint8Array; + "currentSignatureThreshold": bigint; + "currentSignerPrincipal": string; + "currentSignerSet": Uint8Array[]; +}>, + getCurrentSignerPrincipal: {"name":"get-current-signer-principal","access":"read_only","args":[],"outputs":{"type":"principal"}} as TypedAbiFunction<[], string>, + getCurrentSignerSet: {"name":"get-current-signer-set","access":"read_only","args":[],"outputs":{"type":{"list":{"type":{"buffer":{"length":33}},"length":128}}}} as TypedAbiFunction<[], Uint8Array[]>, + getDepositStatus: {"name":"get-deposit-status","access":"read_only","args":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}],"outputs":{"type":{"optional":"bool"}}} as TypedAbiFunction<[txid: TypedAbiArg, voutIndex: TypedAbiArg], boolean | null>, + getWithdrawalRequest: {"name":"get-withdrawal-request","access":"read_only","args":[{"name":"id","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount","type":"uint128"},{"name":"block-height","type":"uint128"},{"name":"max-fee","type":"uint128"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"sender","type":"principal"},{"name":"status","type":{"optional":"bool"}}]}}}} as TypedAbiFunction<[id: TypedAbiArg], { + "amount": bigint; + "blockHeight": bigint; + "maxFee": bigint; + "recipient": { + "hashbytes": Uint8Array; + "version": Uint8Array; +}; + "sender": string; + "status": boolean | null; +} | null>, + isProtocolCaller: {"name":"is-protocol-caller","access":"read_only","args":[{"name":"contract-flag","type":{"buffer":{"length":1}}},{"name":"contract","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[contractFlag: TypedAbiArg, contract: TypedAbiArg], Response> + }, + "maps": { + activeProtocolContracts: {"name":"active-protocol-contracts","key":{"buffer":{"length":1}},"value":"principal"} as TypedAbiMap, + activeProtocolRoles: {"name":"active-protocol-roles","key":"principal","value":{"buffer":{"length":1}}} as TypedAbiMap, + aggregatePubkeys: {"name":"aggregate-pubkeys","key":{"buffer":{"length":33}},"value":"bool"} as TypedAbiMap, + completedDeposits: {"name":"completed-deposits","key":{"tuple":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}]},"value":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}} as TypedAbiMap<{ + "txid": Uint8Array; + "voutIndex": number | bigint; +}, { + "amount": bigint; + "recipient": string; + "sweepBurnHash": Uint8Array; + "sweepBurnHeight": bigint; + "sweepTxid": Uint8Array; +}>, + completedWithdrawalSweep: {"name":"completed-withdrawal-sweep","key":"uint128","value":{"tuple":[{"name":"sweep-burn-hash","type":{"buffer":{"length":32}}},{"name":"sweep-burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}]}} as TypedAbiMap, + depositStatus: {"name":"deposit-status","key":{"tuple":[{"name":"txid","type":{"buffer":{"length":32}}},{"name":"vout-index","type":"uint128"}]},"value":"bool"} as TypedAbiMap<{ + "txid": Uint8Array; + "voutIndex": number | bigint; +}, boolean>, + withdrawalRequests: {"name":"withdrawal-requests","key":"uint128","value":{"tuple":[{"name":"amount","type":"uint128"},{"name":"block-height","type":"uint128"},{"name":"max-fee","type":"uint128"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"sender","type":"principal"}]}} as TypedAbiMap, + withdrawalStatus: {"name":"withdrawal-status","key":"uint128","value":"bool"} as TypedAbiMap + }, + "variables": { + ERR_AGG_PUBKEY_REPLAY: { + name: 'ERR_AGG_PUBKEY_REPLAY', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_REQUEST_ID: { + name: 'ERR_INVALID_REQUEST_ID', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_UNAUTHORIZED: { + name: 'ERR_UNAUTHORIZED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + depositRole: { + name: 'deposit-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + governanceRole: { + name: 'governance-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + withdrawalRole: { + name: 'withdrawal-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable, + currentAggregatePubkey: { + name: 'current-aggregate-pubkey', + type: { + buffer: { + length: 33 + } + }, + access: 'variable' +} as TypedAbiVariable, + currentSignatureThreshold: { + name: 'current-signature-threshold', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable, + currentSignerPrincipal: { + name: 'current-signer-principal', + type: 'principal', + access: 'variable' +} as TypedAbiVariable, + currentSignerSet: { + name: 'current-signer-set', + type: { + list: { + type: { + buffer: { + length: 33 + } + }, + length: 128 + } + }, + access: 'variable' +} as TypedAbiVariable, + lastWithdrawalRequestId: { + name: 'last-withdrawal-request-id', + type: 'uint128', + access: 'variable' +} as TypedAbiVariable + }, + constants: {}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch30","clarity_version":"Clarity3", + contractName: 'sbtc-registry', + }, +sbtcToken: { + "functions": { + protocolMintManyIter: {"name":"protocol-mint-many-iter","access":"private","args":[{"name":"item","type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[item: TypedAbiArg<{ + "amount": number | bigint; + "recipient": string; +}, "item">], Response>, + transferManyIter: {"name":"transfer-many-iter","access":"private","args":[{"name":"individual-transfer","type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"memo","type":{"optional":{"buffer":{"length":34}}}},{"name":"sender","type":"principal"},{"name":"to","type":"principal"}]}},{"name":"result","type":{"response":{"ok":"uint128","error":"uint128"}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[individualTransfer: TypedAbiArg<{ + "amount": number | bigint; + "memo": Uint8Array | null; + "sender": string; + "to": string; +}, "individualTransfer">, result: TypedAbiArg, "result">], Response>, + protocolBurn: {"name":"protocol-burn","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolBurnLocked: {"name":"protocol-burn-locked","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolLock: {"name":"protocol-lock","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolMint: {"name":"protocol-mint","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, recipient: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolMintMany: {"name":"protocol-mint-many","access":"public","args":[{"name":"recipients","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"recipient","type":"principal"}]},"length":200}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":{"list":{"type":{"response":{"ok":"bool","error":"uint128"}},"length":200}},"error":"uint128"}}}} as TypedAbiFunction<[recipients: TypedAbiArg<{ + "amount": number | bigint; + "recipient": string; +}[], "recipients">, contractFlag: TypedAbiArg], Response[], bigint>>, + protocolSetName: {"name":"protocol-set-name","access":"public","args":[{"name":"new-name","type":{"string-ascii":{"length":32}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newName: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolSetSymbol: {"name":"protocol-set-symbol","access":"public","args":[{"name":"new-symbol","type":{"string-ascii":{"length":10}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newSymbol: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolSetTokenUri: {"name":"protocol-set-token-uri","access":"public","args":[{"name":"new-uri","type":{"optional":{"string-utf8":{"length":256}}}},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newUri: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + protocolUnlock: {"name":"protocol-unlock","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"owner","type":"principal"},{"name":"contract-flag","type":{"buffer":{"length":1}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, owner: TypedAbiArg, contractFlag: TypedAbiArg], Response>, + transfer: {"name":"transfer","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"sender","type":"principal"},{"name":"recipient","type":"principal"},{"name":"memo","type":{"optional":{"buffer":{"length":34}}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, sender: TypedAbiArg, recipient: TypedAbiArg, memo: TypedAbiArg], Response>, + transferMany: {"name":"transfer-many","access":"public","args":[{"name":"recipients","type":{"list":{"type":{"tuple":[{"name":"amount","type":"uint128"},{"name":"memo","type":{"optional":{"buffer":{"length":34}}}},{"name":"sender","type":"principal"},{"name":"to","type":"principal"}]},"length":200}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[recipients: TypedAbiArg<{ + "amount": number | bigint; + "memo": Uint8Array | null; + "sender": string; + "to": string; +}[], "recipients">], Response>, + getBalance: {"name":"get-balance","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, + getBalanceAvailable: {"name":"get-balance-available","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, + getBalanceLocked: {"name":"get-balance-locked","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, + getDecimals: {"name":"get-decimals","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[], Response>, + getName: {"name":"get-name","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"string-ascii":{"length":32}},"error":"none"}}}} as TypedAbiFunction<[], Response>, + getSymbol: {"name":"get-symbol","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"string-ascii":{"length":10}},"error":"none"}}}} as TypedAbiFunction<[], Response>, + getTokenUri: {"name":"get-token-uri","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"optional":{"string-utf8":{"length":256}}},"error":"none"}}}} as TypedAbiFunction<[], Response>, + getTotalSupply: {"name":"get-total-supply","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[], Response> + }, + "maps": { + + }, + "variables": { + ERR_NOT_OWNER: { + name: 'ERR_NOT_OWNER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_TRANSFER_INDEX_PREFIX: { + name: 'ERR_TRANSFER_INDEX_PREFIX', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + tokenDecimals: { + name: 'token-decimals', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + tokenName: { + name: 'token-name', + type: { + 'string-ascii': { + length: 32 + } + }, + access: 'variable' +} as TypedAbiVariable, + tokenSymbol: { + name: 'token-symbol', + type: { + 'string-ascii': { + length: 10 + } + }, + access: 'variable' +} as TypedAbiVariable, + tokenUri: { + name: 'token-uri', + type: { + optional: { + 'string-utf8': { + length: 256 + } + } + }, + access: 'variable' +} as TypedAbiVariable + }, + constants: {}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[{"name":"sbtc-token"},{"name":"sbtc-token-locked"}],"epoch":"Epoch30","clarity_version":"Clarity3", + contractName: 'sbtc-token', + }, +sbtcWithdrawal: { + "functions": { + completeIndividualWithdrawalHelper: {"name":"complete-individual-withdrawal-helper","access":"private","args":[{"name":"withdrawal","type":{"tuple":[{"name":"bitcoin-txid","type":{"optional":{"buffer":{"length":32}}}},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"fee","type":{"optional":"uint128"}},{"name":"output-index","type":{"optional":"uint128"}},{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"},{"name":"status","type":"bool"},{"name":"sweep-txid","type":{"optional":{"buffer":{"length":32}}}}]}},{"name":"helper-response","type":{"response":{"ok":"uint128","error":"uint128"}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[withdrawal: TypedAbiArg<{ + "bitcoinTxid": Uint8Array | null; + "burnHash": Uint8Array; + "burnHeight": number | bigint; + "fee": number | bigint | null; + "outputIndex": number | bigint | null; + "requestId": number | bigint; + "signerBitmap": number | bigint; + "status": boolean; + "sweepTxid": Uint8Array | null; +}, "withdrawal">, helperResponse: TypedAbiArg, "helperResponse">], Response>, + acceptWithdrawalRequest: {"name":"accept-withdrawal-request","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"bitcoin-txid","type":{"buffer":{"length":32}}},{"name":"signer-bitmap","type":"uint128"},{"name":"output-index","type":"uint128"},{"name":"fee","type":"uint128"},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"sweep-txid","type":{"buffer":{"length":32}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, bitcoinTxid: TypedAbiArg, signerBitmap: TypedAbiArg, outputIndex: TypedAbiArg, fee: TypedAbiArg, burnHash: TypedAbiArg, burnHeight: TypedAbiArg, sweepTxid: TypedAbiArg], Response>, + completeWithdrawals: {"name":"complete-withdrawals","access":"public","args":[{"name":"withdrawals","type":{"list":{"type":{"tuple":[{"name":"bitcoin-txid","type":{"optional":{"buffer":{"length":32}}}},{"name":"burn-hash","type":{"buffer":{"length":32}}},{"name":"burn-height","type":"uint128"},{"name":"fee","type":{"optional":"uint128"}},{"name":"output-index","type":{"optional":"uint128"}},{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"},{"name":"status","type":"bool"},{"name":"sweep-txid","type":{"optional":{"buffer":{"length":32}}}}]},"length":600}}}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[withdrawals: TypedAbiArg<{ + "bitcoinTxid": Uint8Array | null; + "burnHash": Uint8Array; + "burnHeight": number | bigint; + "fee": number | bigint | null; + "outputIndex": number | bigint | null; + "requestId": number | bigint; + "signerBitmap": number | bigint; + "status": boolean; + "sweepTxid": Uint8Array | null; +}[], "withdrawals">], Response>, + initiateWithdrawalRequest: {"name":"initiate-withdrawal-request","access":"public","args":[{"name":"amount","type":"uint128"},{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}},{"name":"max-fee","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg, recipient: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "recipient">, maxFee: TypedAbiArg], Response>, + rejectWithdrawalRequest: {"name":"reject-withdrawal-request","access":"public","args":[{"name":"request-id","type":"uint128"},{"name":"signer-bitmap","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[requestId: TypedAbiArg, signerBitmap: TypedAbiArg], Response>, + getBurnHeader: {"name":"get-burn-header","access":"read_only","args":[{"name":"height","type":"uint128"}],"outputs":{"type":{"optional":{"buffer":{"length":32}}}}} as TypedAbiFunction<[height: TypedAbiArg], Uint8Array | null>, + validateRecipient: {"name":"validate-recipient","access":"read_only","args":[{"name":"recipient","type":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[recipient: TypedAbiArg<{ + "hashbytes": Uint8Array; + "version": Uint8Array; +}, "recipient">], Response> + }, + "maps": { + + }, + "variables": { + DUST_LIMIT: { + name: 'DUST_LIMIT', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + ERR_ALREADY_PROCESSED: { + name: 'ERR_ALREADY_PROCESSED', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_DUST_LIMIT: { + name: 'ERR_DUST_LIMIT', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_FEE_TOO_HIGH: { + name: 'ERR_FEE_TOO_HIGH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_ADDR_HASHBYTES: { + name: 'ERR_INVALID_ADDR_HASHBYTES', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_ADDR_VERSION: { + name: 'ERR_INVALID_ADDR_VERSION', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_BURN_HASH: { + name: 'ERR_INVALID_BURN_HASH', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_CALLER: { + name: 'ERR_INVALID_CALLER', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_INVALID_REQUEST: { + name: 'ERR_INVALID_REQUEST', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_WITHDRAWAL_INDEX: { + name: 'ERR_WITHDRAWAL_INDEX', + type: { + response: { + ok: 'none', + error: 'uint128' + } + }, + access: 'constant' +} as TypedAbiVariable>, + ERR_WITHDRAWAL_INDEX_PREFIX: { + name: 'ERR_WITHDRAWAL_INDEX_PREFIX', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + MAX_ADDRESS_VERSION: { + name: 'MAX_ADDRESS_VERSION', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + mAX_ADDRESS_VERSION_BUFF_20: { + name: 'MAX_ADDRESS_VERSION_BUFF_20', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + mAX_ADDRESS_VERSION_BUFF_32: { + name: 'MAX_ADDRESS_VERSION_BUFF_32', + type: 'uint128', + access: 'constant' +} as TypedAbiVariable, + withdrawRole: { + name: 'withdraw-role', + type: { + buffer: { + length: 1 + } + }, + access: 'constant' +} as TypedAbiVariable + }, + constants: {}, + "non_fungible_tokens": [ + + ], + "fungible_tokens":[],"epoch":"Epoch30","clarity_version":"Clarity3", + contractName: 'sbtc-withdrawal', } } as const; export const accounts = {"deployer":{"address":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM","balance":"100000000000000"},"wallet_1":{"address":"ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5","balance":"100000000000000"},"wallet_10":{"address":"ST3FFKYTTB975A3JC3F99MM7TXZJ406R3GKE6JV56","balance":"200000000000000"},"wallet_2":{"address":"ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG","balance":"100000000000000"},"wallet_3":{"address":"ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC","balance":"100000000000000"},"wallet_4":{"address":"ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND","balance":"100000000000000"},"wallet_5":{"address":"ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB","balance":"100000000000000"},"wallet_6":{"address":"ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0","balance":"100000000000000"},"wallet_7":{"address":"ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ","balance":"100000000000000"},"wallet_8":{"address":"ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP","balance":"100000000000000"},"wallet_9":{"address":"STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6","balance":"100000000000000"}} as const; -export const identifiers = {"pox5":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5"} as const +export const identifiers = {"pox5":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","pox5Signer":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5-signer","sbtcRegistry":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry","sbtcToken":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token","sbtcWithdrawal":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal"} as const export const simnet = { accounts, @@ -720,7 +1835,7 @@ export const simnet = { } as const; -export const deployments = {"pox5":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","simnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","testnet":null,"mainnet":null}} as const; +export const deployments = {"pox5":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","simnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5","testnet":null,"mainnet":null},"pox5Signer":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5-signer","simnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.pox-5-signer","testnet":null,"mainnet":null},"sbtcRegistry":{"devnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry","simnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry","testnet":null,"mainnet":null},"sbtcToken":{"devnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token","simnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token","testnet":null,"mainnet":null},"sbtcWithdrawal":{"devnet":"ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc-withdrawal","simnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal","testnet":null,"mainnet":"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal"}} as const; export const project = { contracts, diff --git a/stacking/common.ts b/stacking/common.ts index d7be3ac..d7d6401 100644 --- a/stacking/common.ts +++ b/stacking/common.ts @@ -1,13 +1,8 @@ import { StackingClient } from '@stacks/stacking'; import { STACKS_TESTNET } from '@stacks/network'; -import { - getAddressFromPrivateKey, -} from '@stacks/transactions'; +import { ClarityType, deserializeCV, getAddressFromPrivateKey } from '@stacks/transactions'; import { getPublicKeyFromPrivate, publicKeyToBtcAddress } from '@stacks/encryption'; -import { - createClient, -} from '@stacks/blockchain-api-client'; -import { Transaction } from '@stacks/stacks-blockchain-api-types'; +import { createClient } from '@stacks/blockchain-api-client'; import { Logger, pino } from 'pino'; const serviceName = process.env.SERVICE_NAME || 'JS'; @@ -43,7 +38,7 @@ export const apiClient = createClient({ export const EPOCH_30_START = parseEnvInt('STACKS_30_HEIGHT', true); export const EPOCH_25_START = parseEnvInt('STACKS_25_HEIGHT', true); -export const EPOCH_35_START = parseEnvInt('STACKS_35_HEIGHT', true); +export const EPOCH_40_START = parseEnvInt('STACKS_40_HEIGHT', true); export const POX_PREPARE_LENGTH = parseEnvInt('POX_PREPARE_LENGTH', true); export const POX_REWARD_LENGTH = parseEnvInt('POX_REWARD_LENGTH', true); export const WALLET_NAME = 'btc_staking'; @@ -70,9 +65,32 @@ export const accounts = process.env.STACKING_KEYS!.split(',').map((privKey, inde account: stxAddress, index: index, }), + signerManager: `${stxAddress}.signer-manager`, }; }); +export async function fetchAccount(stxAddress: string) { + const url = `${nodeUrl}/v2/accounts/${stxAddress}?proof=0`; + const res = await fetch(url); + const data = (await res.json()) as { + unlock_height: number; + locked: string; + balance: string; + }; + logger.info({ data }, 'Account data'); + const locked = deserializeCV(data.locked.slice(2)); + const balance = deserializeCV(data.balance.slice(2)); + if (locked.type !== ClarityType.Int || balance.type !== ClarityType.Int) { + logger.error({ locked, balance }, 'Invalid account data'); + throw new Error('Invalid account data'); + } + return { + unlockHeight: data.unlock_height, + lockedAmount: BigInt(locked.value), + balance: BigInt(balance.value), + }; +} + export type Account = typeof accounts[0]; export const MAX_U128 = 2n ** 128n - 1n; @@ -82,7 +100,12 @@ export async function waitForSetup() { try { await accounts[0]!.client.getPoxInfo(); } catch (error) { - if (error instanceof Error && 'cause' in error && error.cause instanceof Error && /(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause.message)) { + if ( + error instanceof Error && + 'cause' in error && + error.cause instanceof Error && + /(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause.message) + ) { console.log(`Stacks node not ready, waiting...`); } await new Promise(resolve => setTimeout(resolve, 3000)); @@ -145,4 +168,4 @@ export async function waitForTxConfirmed(txid: string) { } }, 500); }); -} \ No newline at end of file +} diff --git a/stacking/contracts/pox-5-signer.clar b/stacking/contracts/pox-5-signer.clar new file mode 100644 index 0000000..690e73a --- /dev/null +++ b/stacking/contracts/pox-5-signer.clar @@ -0,0 +1,296 @@ +(impl-trait .pox-5.signer-manager-trait) +(use-trait signer-manager-trait .pox-5.signer-manager-trait) + +(define-constant ERR_NO_CLAIMABLE_REWARDS (err u1001)) + +;; Used to prevent fractional multiplication errors +;; during reward calculations +(define-constant PRECISION u1000000000000000000) ;; 1e18 + +;; default to allowing deployer to register as a pool +(define-data-var allowed-caller principal tx-sender) + +(define-map rewards-per-token-for-cycle + { + index: uint, + is-bond: bool, + } + uint +) + +(define-map staker-rewards-paid-per-token-for-cycle + { + is-bond: bool, + index: uint, + staker: principal, + } + uint +) + +;; Represents pending, but unclaimed rewards for a staker +(define-map staker-pending-rewards-for-cycle + { + is-bond: bool, + index: uint, + staker: principal, + } + uint +) + +;; #[allow(unnecessary_public)] +(define-public (validate-stake! + ;; #[allow(unused_binding)] + (staker principal) + ;; #[allow(unused_binding)] + (first-index uint) + ;; #[allow(unused_binding)] + (num-indexes uint) + ;; #[allow(unused_binding)] + (amount-ustx uint) + ;; #[allow(unused_binding)] + (amount-sats uint) + ;; #[allow(unused_binding)] + (is-bond bool) + ;; #[allow(unused_binding)] + (signer-calldata (optional (buff 500))) + ) + (ok true) +) + +(define-public (update-allowed-caller (new-allowed-caller principal)) + (ok (var-set allowed-caller new-allowed-caller)) +) + +(define-public (register-self + (signer-manager ) + (signer-key (buff 33)) + (auth-id uint) + (signer-sig (buff 65)) + ) + (as-contract? () + (try! (contract-call? .pox-5 grant-signer-key signer-key current-contract + auth-id signer-sig + )) + (try! (contract-call? .pox-5 register-signer signer-manager signer-key)) + ) +) + +;; Handling rewards checkpointing for a staker +(define-public (checkpoint-staker + (staker principal) + (first-index uint) + (num-indexes uint) + (is-bond bool) + ) + (begin + (try! (fold checkpoint-staker-for-index + (unwrap-panic (slice? + (list + u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 + u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 + u30 u31 u32 u33 u34 u35 u36 u37 u38 u39 u40 u41 u42 u43 + u44 u45 u46 u47 u48 u49 u50 u51 u52 u53 u54 u55 u56 u57 + u58 u59 u60 u61 u62 u63 u64 u65 u66 u67 u68 u69 u70 u71 + u72 u73 u74 u75 u76 u77 u78 u79 u80 u81 u82 u83 u84 u85 + u86 u87 u88 u89 u90 u91 u92 u93 u94 u95 + ) + u0 num-indexes + )) + (ok { + staker: staker, + first-index: first-index, + is-bond: is-bond, + }) + )) + (ok true) + ) +) + +(define-private (checkpoint-staker-for-index + (index-offset uint) + (acc-res (response { + staker: principal, + first-index: uint, + is-bond: bool, + } + uint + )) + ) + (let ( + (acc (try! acc-res)) + (staker (get staker acc)) + (index (+ (get first-index acc) index-offset)) + ) + (crystallize-staker-rewards staker index (get is-bond acc)) + (ok acc) + ) +) + +(define-private (crystallize-staker-rewards + (staker principal) + (index uint) + (is-bond bool) + ) + (let ( + (earned (get-earned-staker-rewards staker index is-bond)) + (rewards-per-token (get-rewards-per-token-for-cycle index is-bond)) + ) + (map-set staker-pending-rewards-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + } + earned + ) + (map-set staker-rewards-paid-per-token-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + } + rewards-per-token + ) + { + earned: earned, + rewards-per-token: rewards-per-token, + } + ) +) + +(define-public (claim-rewards + (bond-periods (list 6 uint)) + (reward-cycle uint) + ) + (let ((new-rewards-info (try! (as-contract? () + (try! (contract-call? .pox-5 claim-rewards bond-periods reward-cycle)) + )))) + (update-rewards-info + (get rewards-per-token (get stx-rewards new-rewards-info)) false + reward-cycle + ) + (fold update-bond-rewards-info (get bond-rewards new-rewards-info) true) + (ok new-rewards-info) + ) +) + +;; Get the total amount of rewards earned since the last +;; rewards snapshot for this staker. +;; +;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` +(define-read-only (get-earned-staker-rewards + (staker principal) + (index uint) + (is-bond bool) + ) + (let ( + (shares (contract-call? .pox-5 get-staker-shares-staked-for-cycle staker + index is-bond current-contract + )) + (rpt-current (get-rewards-per-token-for-cycle index is-bond)) + (rpt-paid (get-staker-rewards-per-token-paid-for-cycle staker index is-bond)) + (pending (get-staker-pending-rewards-for-cycle staker index is-bond)) + (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) + ) + (+ pending newly-earned) + ) +) + +(define-public (claim-staker-rewards + (index uint) + (is-bond bool) + ) + (let ( + (staker tx-sender) + (rewards-info (crystallize-staker-rewards staker index is-bond)) + (earned (get earned rewards-info)) + ) + (asserts! (> earned u0) ERR_NO_CLAIMABLE_REWARDS) + (map-set staker-pending-rewards-for-cycle { + staker: staker, + is-bond: is-bond, + index: index, + } + u0 + ) + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" earned + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer earned tx-sender staker none + )) + )) + (ok earned) + ) +) + +(define-private (update-rewards-info + (rewards-per-share uint) + (is-bond bool) + (index uint) + ) + (begin + (map-set rewards-per-token-for-cycle { + index: index, + is-bond: is-bond, + } + rewards-per-share + ) + ) +) + +(define-private (update-bond-rewards-info + (bond-info { + bond-index: uint, + earned: uint, + rewards-per-token: uint, + }) + ;; #[allow(unused_binding)] + (acc bool) + ) + (map-set rewards-per-token-for-cycle { + is-bond: true, + index: (get bond-index bond-info), + } + (get rewards-per-token bond-info) + ) +) + +(define-read-only (get-rewards-per-token-for-cycle + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? rewards-per-token-for-cycle { + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-staker-rewards-per-token-paid-for-cycle + (staker principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? staker-rewards-paid-per-token-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-staker-pending-rewards-for-cycle + (staker principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? staker-pending-rewards-for-cycle { + staker: staker, + index: index, + is-bond: is-bond, + }) + ) +) diff --git a/stacking/contracts/pox-5.clar b/stacking/contracts/pox-5.clar index ef700bd..4324dd0 100644 --- a/stacking/contracts/pox-5.clar +++ b/stacking/contracts/pox-5.clar @@ -1,55 +1,73 @@ -;; The caller is already staked -(define-constant ERR_ALREADY_STAKED (err u1)) -;; The caller is not staked -(define-constant ERR_NOT_STAKED (err u2)) -;; The caller does not have sufficient STX to stake -(define-constant ERR_INSUFFICIENT_FUNDS (err u4)) -;; The `start-burn-ht` is not valid - it must be in the next reward cycle -(define-constant ERR_INVALID_START_BURN_HEIGHT (err u8)) -;; The `num-cycles` provided is invalid - it must be less than MAX_NUM_CYCLES -(define-constant ERR_INVALID_NUM_CYCLES (err u9)) -;; The stacker tried to call `stake-extend` but not during their last cycle -(define-constant ERR_CANNOT_EXTEND (err u10)) -(define-constant ERR_INVALID_AMOUNT (err u11)) -(define-constant ERR_INVALID_POX_ADDRESS (err u13)) -(define-constant ERR_POOL_NOT_FOUND (err u14)) -;; The signer key grant has already been used -(define-constant ERR_SIGNER_KEY_GRANT_USED (err u15)) -(define-constant ERR_INVALID_SIGNATURE_RECOVER (err u16)) -(define-constant ERR_INVALID_SIGNATURE_PUBKEY (err u17)) -(define-constant ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH (err u19)) -(define-constant ERR_SIGNER_AUTH_USED (err u20)) -(define-constant ERR_SIGNER_KEY_GRANT_NOT_FOUND (err u21)) -(define-constant ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH (err u22)) -(define-constant ERR_NOT_ALLOWED (err u23)) - -(define-trait pool-owner-trait ( - (validate-stake! - ;; caller, amount-ustx, num-cycles, unlock-bytes - (principal uint uint (buff 683)) - (response bool uint) - ) - (validate-management! - ;; caller, signer-key, pox-addr - (principal (buff 33) { - version: (buff 1), - hashbytes: (buff 32), - }) - (response bool uint) - ) -)) - -;; Values for stacks address versions -;; #[allow(unused_const)] -(define-constant STACKS_ADDR_VERSION_MAINNET 0x16) -;; #[allow(unused_const)] -(define-constant STACKS_ADDR_VERSION_TESTNET 0x1a) - -;; Maximum number of cycles you can stake for -(define-constant MAX_NUM_CYCLES u24) - -;; Minimum amount of uSTX you can stake -(define-constant MIN_STACKING_AMOUNT u100000000) ;; 100 STX +(define-constant ERR_UNAUTHORIZED (err u1)) +(define-constant ERR_CANNOT_SETUP_BOND_TOO_SOON (err u2)) +(define-constant ERR_CANNOT_SETUP_BOND_TOO_LATE (err u3)) +(define-constant ERR_BOND_ALREADY_SETUP (err u4)) +(define-constant ERR_STAKER_ALREADY_ADDED (err u5)) +(define-constant ERR_L1_LOCKUP_NOT_FOUND (err u6)) +(define-constant ERR_BOND_NOT_FOUND (err u7)) +(define-constant ERR_INSUFFICIENT_STX (err u8)) +(define-constant ERR_ALREADY_REGISTERED (err u9)) +(define-constant ERR_TOO_MUCH_SATS (err u10)) +(define-constant ERR_NOT_ALLOWLISTED (err u11)) +(define-constant ERR_SIGNER_KEY_GRANT_USED (err u12)) +(define-constant ERR_INVALID_SIGNATURE_RECOVER (err u13)) +(define-constant ERR_INVALID_SIGNATURE_PUBKEY (err u14)) +(define-constant ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH (err u15)) +(define-constant ERR_SIGNER_AUTH_USED (err u16)) +(define-constant ERR_SIGNER_KEY_GRANT_NOT_FOUND (err u17)) +(define-constant ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH (err u18)) +(define-constant ERR_ALREADY_STAKED (err u19)) +(define-constant ERR_INVALID_NUM_CYCLES (err u20)) +(define-constant ERR_INVALID_POX_ADDRESS (err u21)) +(define-constant ERR_UNAUTHORIZED_CALLER (err u22)) +(define-constant ERR_SIGNER_NOT_FOUND (err u23)) +(define-constant ERR_INVALID_START_BURN_HEIGHT (err u24)) +(define-constant ERR_NO_SBTC_BALANCE (err u25)) +(define-constant ERR_UNAUTHORIZED_SIGNER_REGISTRATION (err u26)) +(define-constant ERR_NOT_STAKING (err u27)) +(define-constant ERR_UNSTAKE_IN_PREPARE_PHASE (err u28)) +;; Trying to pay out to bonds in an invalid order +(define-constant ERR_INVALID_BOND_PERIOD_ORDERING (err u29)) +;; We already calculated at the start of this cycle +(define-constant ERR_DISTRIBUTION_ALREADY_COMPUTED (err u30)) +(define-constant ERR_BOND_NOT_ACTIVE (err u31)) +(define-constant ERR_NO_CLAIMABLE_REWARDS (err u32)) +(define-constant ERR_ACTIVE_BOND_NOT_INCLUDED (err u33)) +;; Not actively in a bond +(define-constant ERR_NOT_BOND_PARTICIPANT (err u34)) +;; A call to announce an early unlock was made +;; for a bond membership that has an L2 lockup +(define-constant ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK (err u35)) +;; The argument provided does not match the staker's signer +(define-constant ERR_INVALID_OLD_SIGNER_MANAGER (err u36)) +;; The amount of sats provided to unstake is invalid +(define-constant ERR_INVALID_UNSTAKE_SBTC_AMOUNT (err u37)) +;; The bond participant did not stake sBTC +(define-constant ERR_CANNOT_UNSTAKE_SBTC (err u38)) +;; A parse error occurred when reading a Bitcoin header +(define-constant ERR_READ_TX_OUT_OF_BOUNDS (err u39)) +;; An incorrect Bitcoin header was provided as part of a lockup proof +(define-constant ERR_INVALID_BTC_HEADER (err u40)) +;; An incorrect merkle proof was provided as part of a lockup proof +(define-constant ERR_INVALID_MERKLE_PROOF (err u41)) +;; The output script provided is incorrect +(define-constant ERR_INVALID_LOCKUP_SCRIPT (err u42)) +;; A staker tried to register for a bond after it already started +(define-constant ERR_BOND_ALREADY_STARTED (err u43)) +;; Cannot call `update-bond-registration` with the same signer +(define-constant ERR_UPDATE_BOND_SAME_SIGNER (err u44)) + +;; The length, in terms of staking cycles, of a given +;; bond period +(define-constant BOND_LENGTH_CYCLES u12) +;; The gap between the start of different bond periods +(define-constant BOND_GAP_CYCLES u2) +;; The maximum amount of time that a user can stake for +(define-constant MAX_NUM_CYCLES u96) + +;; The minimum amount of uSTX that a staker must stake +;; to become part of the signer set +(define-constant SIGNER_SET_MIN_USTX u50000000000) ;; 50k STX ;; SIP18 message prefix (define-constant SIP018_MSG_PREFIX 0x534950303138) @@ -71,101 +89,156 @@ ;; (0x05 and 0x06 have 32-byte hashbytes) (define-constant MAX_ADDRESS_VERSION_BUFF_32 u6) -;; Default length of the PoX registration window, in burnchain blocks. -(define-constant PREPARE_CYCLE_LENGTH (if is-in-mainnet - u100 - u50 -)) +;; Values for stacks address versions +(define-constant STACKS_ADDR_VERSION_MAINNET 0x16) +(define-constant STACKS_ADDR_VERSION_TESTNET 0x1a) -;; Default length of the PoX reward cycle, in burnchain blocks. -(define-constant REWARD_CYCLE_LENGTH (if is-in-mainnet - u2100 - u1050 -)) +;; Used to prevent fractional multiplication errors +;; during reward calculations +(define-constant PRECISION u1000000000000000000) ;; 1e18 -;; Data vars that store a copy of the burnchain configuration. -;; Implemented as data-vars, so that different configurations can be -;; used in e.g. test harnesses. -;; #[allow(unused_data_var)] -(define-data-var pox-prepare-cycle-length uint PREPARE_CYCLE_LENGTH) -(define-data-var pox-reward-cycle-length uint REWARD_CYCLE_LENGTH) -(define-data-var first-burnchain-block-height uint u0) -(define-data-var configured bool false) -;; #[allow(unused_data_var)] -(define-data-var first-pox-5-reward-cycle uint u0) +;; The % of rewards that go to reserve, expressed +;; in basis points +(define-constant RESERVE_RATIO u1500) -;; This function can only be called once, when it boots up -(define-public (set-burnchain-parameters - (first-burn-height uint) - (prepare-cycle-length uint) - (reward-cycle-length uint) - (begin-pox5-reward-cycle uint) - ) - (begin - (unwrap-panic (if (var-get configured) - (err false) - (ok true) - )) - (var-set first-burnchain-block-height first-burn-height) - (var-set pox-prepare-cycle-length prepare-cycle-length) - (var-set pox-reward-cycle-length reward-cycle-length) - (var-set first-pox-5-reward-cycle begin-pox5-reward-cycle) - (var-set configured true) - (ok true) - ) +;; Core properties of protocol bonds +(define-map protocol-bonds + uint + { + ;; target yield rate (apy) in basis points + target-rate: uint, + ;; representation of STX:BTC price + ;; this value is equal to "ustx per 100 sats", which + ;; also translates to `(BTCUSD / STXUSD)`. + ;; used to determine bond priority + stx-value-ratio: uint, + ;; minimum amount of STX that must be locked + ;; relative to BTC for this term. + ;; Represented in basis points. + min-ustx-ratio: uint, + ;; The allowed early unlock signers for this bond period + early-unlock-signers: (buff 683), + ;; The Stacks principal that can announce early L1 unlocks + early-unlock-admin: principal, + } ) -;; Users can stake to a pool, where the pool owner -;; (which is the key of this map) is able to manage -;; the signer key and pox address for the pool. -(define-map pools - principal +(define-map protocol-bond-allowances { - signer-key: (buff 33), - pox-addr: { - version: (buff 1), - hashbytes: (buff 32), - }, + bond-index: uint, + staker: principal, } + ;; max amount of sats they can contribute + uint ) -(define-map staking-state +(define-map protocol-bond-memberships principal { - num-cycles: uint, - unlock-bytes: (buff 683), + bond-index: uint, amount-ustx: uint, - first-reward-cycle: uint, - pool-or-solo-info: (response principal { - pox-addr: { - version: (buff 1), - hashbytes: (buff 32), - }, - signer-key: (buff 33), - }), + signer: principal, + is-l1-lock: bool, } ) +;; Total amount of sats staked per bond period +(define-map protocol-bonds-total-staked + uint + uint +) + (define-map signer-key-grants { signer-key: (buff 33), - staker: principal, + signer-manager: principal, } - (optional { - version: (buff 1), - hashbytes: (buff 32), - }) + bool ) (define-map used-signer-key-grants { signer-key: (buff 33), - staker: principal, + signer-manager: principal, auth-id: uint, } bool ) +;; Users can stake to a signer, where the signer owner +;; (which is the key of this map) is able to manage +;; the signer key for the signer. +(define-map signers + principal + (buff 33) ;; signer key +) + +;; Keep track of how much total STX has been delegated for a signer +;; for a given cycle. This includes from both protocol bonds and STX-only +;; stakers. This is the value that should be used to determine signer weight +;; when approving blocks. +(define-map signer-delegated-per-cycle + { + signer: principal, + cycle: uint, + } + uint +) + +;; Keep track of much much total STX has been staked, only through +;; STX-only signing, for this cycle. This may differ from +;; `signer-shares-staked-for-cycle`, which will be 0 if the total +;; amount delegated to this signer is below `SIGNER_SET_MIN_USTX`. +;; +;; Do not use for reward calculations! +(define-map signer-pending-staked-ustx-per-cycle + { + signer: principal, + cycle: uint, + } + uint +) + +;; Keep track of a staker's high-level info +(define-map staker-info + principal + { + amount-ustx: uint, + first-reward-cycle: uint, + num-cycles: uint, + signer: principal, + } +) + +;; Per-cycle staker signer membership. Only used for stx-only staking. +(define-map staker-signer-cycle-memberships + { + staker: principal, + cycle: uint, + } + { + amount-ustx: uint, + signer: principal, + } +) + +;; This represents the total uSTX delegated (through both +;; protocol bonds and STX-only staking) for a cycle +(define-map ustx-delegated-per-cycle + uint + uint +) + +;; allowed contract-callers +(define-map allowance-contract-callers + { + sender: principal, + contract-caller: principal, + } + ;; Optional expiration burn height + (optional uint) +) + ;; State for tracking used signer key authorizations. This prevents re-use ;; of the same signature or pre-set authorization for multiple transactions. ;; Refer to the `signer-key-authorizations` map for the documentation on these fields @@ -175,461 +248,1728 @@ reward-cycle: uint, period: uint, topic: (string-ascii 14), - pox-addr: { + pox-addr: (optional { version: (buff 1), hashbytes: (buff 32), - }, + }), auth-id: uint, max-amount: uint, } bool ;; Whether the field has been used or not ) -;; What's the reward cycle number of the burnchain block height? -;; Will runtime-abort if height is less than the first burnchain block (this is intentional) -(define-read-only (burn-height-to-reward-cycle (height uint)) - (/ (- height (var-get first-burnchain-block-height)) - (var-get pox-reward-cycle-length) - ) +;; State to track the per-share rewards earned for bond periods +;; and reward cycles. This value must only increment +(define-map rewards-per-token-for-cycle + { + is-bond: bool, + index: uint, + } + uint ) -;; What's the block height at the start of a given reward cycle? -(define-read-only (reward-cycle-to-burn-height (cycle uint)) - (+ (var-get first-burnchain-block-height) - (* cycle (var-get pox-reward-cycle-length)) - ) +;; Total shares (either ustx or sats) staked in a given +;; bond or stx-only cycle +(define-map total-shares-staked-for-cycle + { + index: uint, + is-bond: bool, + } + uint ) -;; Get the L1 unlock height for a given reward cycle. -;; This is equal to exactly halfway through the provided cycle. -(define-read-only (reward-cycle-to-unlock-height (cycle uint)) - (+ (reward-cycle-to-burn-height cycle) - (/ (var-get pox-reward-cycle-length) u2) - ) +;; State to track the per-staker shares for a given signer. +(define-map staker-shares-staked-for-cycle + { + index: uint, + is-bond: bool, + staker: principal, + signer: principal, + } + uint ) -;; What's the current PoX reward cycle? -(define-read-only (current-pox-reward-cycle) - (burn-height-to-reward-cycle burn-block-height) +;; Amount of shares staked for a given signer in a given cycle. +;; This is strictly for reward calculations - ie the STX +;; from Bitcoin staking are not accounted for here. +(define-map signer-shares-staked-for-cycle + { + index: uint, + is-bond: bool, + signer: principal, + } + uint ) -;; Get the _current_ PoX staking principal information. If the information -;; is expired, or if there's never been such a staker, then returns none. -(define-read-only (get-staker-info (staker principal)) - (match (map-get? staking-state staker) - staking-info - (if (<= - (+ (get first-reward-cycle staking-info) - (get num-cycles staking-info) - ) - (current-pox-reward-cycle) - ) - ;; present, but lock has expired - none - ;; present, and lock has not expired - (some staking-info) - ) - ;; no state at all - none - ) +;; Represents a snapshot of `rewards-per-token` at the last +;; time of rewards calculation for this specific signer +(define-map signer-rewards-per-token-paid-for-cycle + { + is-bond: bool, + index: uint, + signer: principal, + } + uint ) -(define-read-only (get-pool-info (owner principal)) - (map-get? pools owner) +;; Represents pending, but unclaimed rewards for a signer +(define-map signer-pending-rewards-for-cycle + { + is-bond: bool, + index: uint, + signer: principal, + } + uint ) -(define-read-only (get-pox-info) - (ok { - min-amount-ustx: MIN_STACKING_AMOUNT, - reward-cycle-id: (current-pox-reward-cycle), - prepare-cycle-length: (var-get pox-prepare-cycle-length), - first-burnchain-block-height: (var-get first-burnchain-block-height), - reward-cycle-length: (var-get pox-reward-cycle-length), - total-liquid-supply-ustx: stx-liquid-supply, - }) -) +;; The role that is allowed to set bond parameters. +;; On non-mainnet networks `make_pox_5_body` rewrites the literal to the +;; configured admin before deploy. +;; TODO: this should be set to some predefined multisig for mainnet. +(define-data-var bond-admin principal 'SP000000000000000000002Q6VF78) + +;; Data vars that store a copy of the burnchain configuration. +;; Implemented as data-vars, so that different configurations can be +;; used in e.g. test harnesses. +;; #[allow(unused_data_var)] +(define-data-var pox-prepare-cycle-length uint (if is-in-mainnet + u100 + u50 +)) +(define-data-var pox-reward-cycle-length uint (if is-in-mainnet + u2100 + u1050 +)) +(define-data-var first-burnchain-block-height uint u0) +(define-data-var configured bool false) +;; The first reward cycle where pox-5 is active. This +;; is also equal to the first bond period. +(define-data-var first-pox-5-reward-cycle uint u0) +;; The first reward cycle where the first bond period occurs +(define-data-var first-bond-period-cycle uint u0) -;;; Public functions +;;;; The last accounted balance (of sBTC held by this contract) +;;;; at a time of reward computation. +;;;; N.B. it is critical that this value is set to the contract's +;;;; sBTC balance after any transfer of sBTC out of this contract. +;; (define-data-var last-accounted-balance uint u0) -(define-public (stake-pooled - (pool-owner ) - (amount-ustx uint) - (num-cycles uint) - (unlock-bytes (buff 683)) - (start-burn-ht uint) +;; The last accounted balance of rewards. Used to keep +;; track of which sBTC is just for rewards, vs from +;; staking. +(define-data-var last-accounted-rewards-only uint u0) + +;; The last burn height in which rewards were calculated +(define-data-var last-reward-compute-height uint u0) + +;; the amount of sBTC claimable by the reserve +(define-data-var reserve-balance uint u0) + +;; The total amount of sBTC staked +(define-data-var total-sbtc-staked uint u0) + +(define-trait signer-manager-trait ( + (validate-stake! + ;; staker, first-index, num-indexes, amount-ustx, amount-sats, is-bond, signer-calldata + (principal uint uint uint uint bool (optional (buff 500))) + (response bool uint) ) - (let ((owner (contract-of pool-owner))) - (asserts! (is-some (get-pool-info owner)) ERR_POOL_NOT_FOUND) - (try! (contract-call? pool-owner validate-stake! tx-sender amount-ustx - num-cycles unlock-bytes - )) - (inner-stake amount-ustx num-cycles unlock-bytes start-burn-ht (ok owner)) + (checkpoint-staker + ;; staker, first-index, num-indexes, is-bond + (principal uint uint bool) + (response bool uint) ) -) +)) -;; #[allow(unnecessary_public)] -(define-public (stake - (amount-ustx uint) - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), - }) - (start-burn-ht uint) - (signer-sig (optional (buff 65))) - (signer-key (buff 33)) - (max-amount uint) - (auth-id uint) - (num-cycles uint) - (unlock-bytes (buff 683)) +;; This function can only be called once, when it boots up +(define-public (set-burnchain-parameters + (first-burn-height uint) + (prepare-cycle-length uint) + (reward-cycle-length uint) + (begin-pox5-reward-cycle uint) ) - ;; this stacker's first reward cycle is the _next_ reward cycle (begin - ;; pox-addr must be valid - (try! (check-pox-addr pox-addr)) - - (try! (validate-signer-key-usage pox-addr (current-pox-reward-cycle) "stake" - num-cycles signer-sig signer-key amount-ustx max-amount auth-id - tx-sender + (unwrap-panic (if (var-get configured) + (err false) + (ok true) )) + (var-set first-burnchain-block-height first-burn-height) + (var-set pox-prepare-cycle-length prepare-cycle-length) + (var-set pox-reward-cycle-length reward-cycle-length) + (var-set first-pox-5-reward-cycle begin-pox5-reward-cycle) + (var-set first-bond-period-cycle begin-pox5-reward-cycle) + (var-set configured true) + (ok true) + ) +) - (inner-stake amount-ustx num-cycles unlock-bytes start-burn-ht - (err { - pox-addr: pox-addr, - signer-key: signer-key, - }) - ) +(define-public (set-bond-admin (new-admin principal)) + (begin + ;; only bond admin can call this. + (asserts! (is-eq contract-caller (var-get bond-admin)) ERR_UNAUTHORIZED) + (ok (var-set bond-admin new-admin)) ) ) -(define-private (inner-stake - (amount-ustx uint) - (num-cycles uint) - (unlock-bytes (buff 683)) - (start-burn-ht uint) - (pool-or-solo-info (response principal { - pox-addr: { - version: (buff 1), - hashbytes: (buff 32), - }, - signer-key: (buff 33), +;; Setup a new protocol bond by providing parameters and the +;; allowlist for the bond. +;; +;; @param target-rate; target yield rate (apy) in basis points +;; @param stx-value-ratio; representation of STX:BTC price +;; @param min-ustx-ratio; minimum amount of STX that must be locked +;; relative to BTC for this term. Represented in basis points. +;; @param early-exit-signers: An allowlist of bond members that can +;; participate in the bond. +;; +;; This function can only be called once. +(define-public (setup-bond + (bond-index uint) + (target-rate uint) + (stx-value-ratio uint) + (min-ustx-ratio uint) + (early-unlock-signers (buff 683)) + (early-unlock-admin principal) + (allowlist (list 1000 { + staker: principal, + max-sats: uint, })) ) - (let ( - (current-cycle (current-pox-reward-cycle)) - (first-reward-cycle (+ u1 current-cycle)) - (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) - (unlock-cycle (+ current-cycle num-cycles)) - (unlock-burn-height (reward-cycle-to-unlock-height unlock-cycle)) + (let ((bond-start-height (bond-period-to-burn-height bond-index))) + ;; only bond admin can call this. + (asserts! (is-eq contract-caller (var-get bond-admin)) ERR_UNAUTHORIZED) + + ;; only can be called within 2 cycles of bond start + (asserts! + (or + ;; prevent underflow + (< bond-start-height + (* BOND_GAP_CYCLES (var-get pox-reward-cycle-length)) + ) + (<= + (- bond-start-height + (* BOND_GAP_CYCLES (var-get pox-reward-cycle-length)) + ) + burn-block-height + ) + ) + ERR_CANNOT_SETUP_BOND_TOO_SOON ) - ;; the start-burn-ht must result in the next reward cycle, do not allow stackers - ;; to "post-date" their `stack-stx` transaction - (asserts! (is-eq first-reward-cycle specified-reward-cycle) - ERR_INVALID_START_BURN_HEIGHT + + ;; only can be called before bond start + (asserts! (< burn-block-height bond-start-height) + ERR_CANNOT_SETUP_BOND_TOO_LATE ) - ;; amount must be valid - (asserts! (>= amount-ustx MIN_STACKING_AMOUNT) ERR_INVALID_AMOUNT) + (asserts! + (map-insert protocol-bonds bond-index { + target-rate: target-rate, + stx-value-ratio: stx-value-ratio, + min-ustx-ratio: min-ustx-ratio, + early-unlock-signers: early-unlock-signers, + early-unlock-admin: early-unlock-admin, + }) + ERR_BOND_ALREADY_SETUP + ) + + (let ((accumulator (try! (fold add-staker-to-bond allowlist + (ok { + sum-max-sats: u0, + bond-index: bond-index, + }) + )))) + (ok { + bond-index: bond-index, + target-rate: target-rate, + stx-value-ratio: stx-value-ratio, + min-ustx-ratio: min-ustx-ratio, + early-unlock-signers: early-unlock-signers, + max-allocation-sats: (get sum-max-sats accumulator), + }) + ) + ) +) + +(define-private (add-staker-to-bond + (staker-item { + staker: principal, + max-sats: uint, + }) + (accumulator-res (response { + sum-max-sats: uint, + bond-index: uint, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (bond-index (get bond-index accumulator)) + ) + (asserts! + (map-insert protocol-bond-allowances { + bond-index: bond-index, + staker: (get staker staker-item), + } + (get max-sats staker-item) + ) + ERR_STAKER_ALREADY_ADDED + ) + (print (merge staker-item { + topic: "add-to-allowlist", + bond-index: bond-index, + })) + (ok { + sum-max-sats: (+ (get sum-max-sats accumulator) (get max-sats staker-item)), + bond-index: bond-index, + }) + ) +) + +;; Register for a protocol bond. In order the call this function, +;; the bond must already have been created, and `contract-caller` +;; must be in the allowlist. +;; +;; The caller must either provide sBTC that they want to lockup, +;; or they must provide proof of their L1 BTC lockup. +(define-public (register-for-bond + (bond-index uint) + (signer-manager ) + (amount-ustx uint) + ;; Their BTC lockup info. If the response is `ok`, then + ;; this is a list of outputs corresponding to their timelocks. + ;; If the response is `err`, this is the amount of sBTC (in sats) + ;; that they want to lock. + (btc-lockup (response { + outputs: (list 10 + { + height: uint, + tx: (buff 100000), + output-index: uint, + header: (buff 80), + leaf-hashes: (list 14 (buff 32)), + tx-count: uint, + tx-index: uint, + amount: uint, + } + ), + unlock-bytes: (buff 683), + } + uint + )) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (sats-total (try! (match btc-lockup + l1-lockups (verify-l1-lockups tx-sender bond-index l1-lockups) + sbtc-amount (lock-sbtc sbtc-amount) + ))) + (bond (unwrap! (map-get? protocol-bonds bond-index) ERR_BOND_NOT_FOUND)) + (allowance (unwrap! + (map-get? protocol-bond-allowances { + staker: tx-sender, + bond-index: bond-index, + }) + ERR_NOT_ALLOWLISTED + )) + (first-reward-cycle (bond-period-to-reward-cycle bond-index)) + (bond-start-height (bond-period-to-burn-height bond-index)) + ;; the first cycle in which their stx are unlocked + (unlock-cycle (+ first-reward-cycle BOND_LENGTH_CYCLES)) + (current-total-staked (get-total-shares-staked-for-cycle bond-index true)) + (current-signer-staked (get-signer-shares-staked-for-cycle signer bond-index true)) + ) + ;; Verify that they're sending enough STX + (asserts! + (>= amount-ustx + (min-ustx-for-sats-amount sats-total (get stx-value-ratio bond) + (get min-ustx-ratio bond) + )) + ERR_INSUFFICIENT_STX + ) + + ;; Verify that the bond hasn't started + (asserts! (< burn-block-height bond-start-height) + ERR_BOND_ALREADY_STARTED + ) + + ;; Cannot be already staked + (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) + + (asserts! (<= sats-total allowance) ERR_TOO_MUCH_SATS) + + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender bond-index u1 + amount-ustx sats-total true signer-calldata + )) + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + (asserts! (is-none (get-bond-membership tx-sender)) + ERR_ALREADY_REGISTERED + ) + + (map-set protocol-bond-memberships tx-sender { + bond-index: bond-index, + amount-ustx: amount-ustx, + signer: signer, + is-l1-lock: (is-ok btc-lockup), + }) + (map-set protocol-bonds-total-staked bond-index + (+ current-total-staked sats-total) + ) + (crystallize-rewards signer bond-index true) + (map-set total-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + } + (+ current-total-staked sats-total) + ) + (map-set signer-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + signer: signer, + } + (+ current-signer-staked sats-total) + ) + (map-set staker-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + staker: tx-sender, + signer: signer, + } + sats-total + ) + + (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle + BOND_LENGTH_CYCLES amount-ustx false + )) + + (ok { + signer: signer, + staker: tx-sender, + amount-ustx: amount-ustx, + bond-index: bond-index, + first-reward-cycle: first-reward-cycle, + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + unlock-cycle: unlock-cycle, + }) + ) +) + +;; As a bond participant, update your signer. This takes effect +;; in the next reward cycle where this bond participant is active. +;; +;; Note that if the bond hasn't started yet, it's possible for the staker +;; to not be active in the next reward cycle. In that case, the signer is updated +;; from the start of the bond period. +(define-public (update-bond-registration + (signer-manager ) + (old-signer-manager ) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (old-signer (contract-of old-signer-manager)) + (current-membership (unwrap! (get-bond-membership tx-sender) ERR_NOT_BOND_PARTICIPANT)) + (current-signer (get signer current-membership)) + (bond-index (get bond-index current-membership)) + (amount-sats (get-staker-shares-staked-for-cycle tx-sender bond-index true + current-signer + )) + (bond-start-cycle (bond-period-to-reward-cycle bond-index)) + (bond-end-cycle (bond-period-to-reward-cycle (+ bond-index u6))) + (next-cycle (+ (current-pox-reward-cycle) u1)) + (current-signer-total-sats (get-signer-shares-staked-for-cycle current-signer bond-index true)) + (new-signer-total-sats (get-signer-shares-staked-for-cycle signer bond-index true)) + ;; If the bond hasn't started yet, then the first cycle where + ;; this new signer is active is the start cycle. Otherwise, it's the next reward + ;; cycle. In other words, `max(bond-start-cycle, current-cycle + 1)` + (first-reward-cycle (if (> bond-start-cycle next-cycle) + bond-start-cycle + next-cycle + )) + (num-cycles (- bond-end-cycle first-reward-cycle)) + ) + (asserts! (is-eq old-signer current-signer) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + + ;; Validate that the new signer is different + (asserts! (not (is-eq signer old-signer)) ERR_UPDATE_BOND_SAME_SIGNER) + + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender bond-index u1 + (get amount-ustx current-membership) amount-sats true + signer-calldata + )) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker tx-sender bond-index + u1 true + ) + ok-val ok-val + err-val true + ) + + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + (crystallize-rewards current-signer bond-index true) + (crystallize-rewards signer bond-index true) + + ;; Remove the staker from all existing cycles + (try! (remove-staker-from-cycles tx-sender first-reward-cycle num-cycles false)) + + ;; Re-add to existing cycles with the new signer + (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle + num-cycles (get amount-ustx current-membership) false + )) + + ;; Remove the sBTC shares from the current signer + (map-delete staker-shares-staked-for-cycle { + index: bond-index, + staker: tx-sender, + signer: current-signer, + is-bond: true, + }) + (map-set signer-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + signer: current-signer, + } + (- current-signer-total-sats amount-sats) + ) + + ;; Add the sBTC shares to the current signer + (map-set staker-shares-staked-for-cycle { + index: bond-index, + staker: tx-sender, + signer: signer, + is-bond: true, + } + amount-sats + ) + (map-set signer-shares-staked-for-cycle { + index: bond-index, + signer: signer, + is-bond: true, + } + (+ new-signer-total-sats amount-sats) + ) + (map-set protocol-bond-memberships tx-sender { + bond-index: bond-index, + amount-ustx: (get amount-ustx current-membership), + signer: signer, + is-l1-lock: (get is-l1-lock current-membership), + }) + + (ok true) + ) +) + +;; Register a signer +(define-public (register-signer + (signer-manager ) + (signer-key (buff 33)) + ) + (let ((signer (contract-of signer-manager))) + ;; Because signers can have members register at any time, + ;; they must use signer key grants instead of per-tx + ;; authorizations. + (try! (verify-signer-key-grant signer signer-key)) + + ;; Only the signer contract itself can register itself + (asserts! (is-eq tx-sender signer) ERR_UNAUTHORIZED_SIGNER_REGISTRATION) + + (map-set signers signer signer-key) + (ok { + signer: signer, + signer-key: signer-key, + }) + ) +) + +;; Stake your STX +(define-public (stake + (signer-manager ) + (amount-ustx uint) + (num-cycles uint) + (start-burn-ht uint) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (current-cycle (current-pox-reward-cycle)) + (first-reward-cycle (+ u1 current-cycle)) + (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) + ;; the first cycle in which their stx are unlocked + (unlock-cycle (+ first-reward-cycle num-cycles)) + ) + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender + first-reward-cycle num-cycles amount-ustx u0 false + signer-calldata + )) + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; to "post-date" their transaction + (asserts! (is-eq first-reward-cycle specified-reward-cycle) + ERR_INVALID_START_BURN_HEIGHT + ) ;; lock period must be in acceptable range. (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) ;;;; must be called directly by the tx-sender or by an allowed contract-caller - ;; (asserts! (check-caller-allowed) (err ERR_STACKING_PERMISSION_DENIED)) + (try! (check-caller-allowed)) - ;;;; tx-sender principal must not be stacking + ;; Cannot be already staked (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) + ;;;; tx-sender principal must not be in a bond membership + (asserts! (is-none (get-bond-membership tx-sender)) ERR_ALREADY_STAKED) + ;;;; the Stacker must have sufficient unlocked funds (asserts! (>= (stx-get-balance tx-sender) amount-ustx) - ERR_INSUFFICIENT_FUNDS + ERR_INSUFFICIENT_STX ) - (try! (add-staker-to-reward-cycles tx-sender first-reward-cycle num-cycles)) + (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle + num-cycles amount-ustx true + )) - (map-set staking-state tx-sender { + (map-set staker-info tx-sender { amount-ustx: amount-ustx, - unlock-bytes: unlock-bytes, first-reward-cycle: first-reward-cycle, num-cycles: num-cycles, - pool-or-solo-info: pool-or-solo-info, + signer: signer, }) (ok { - stacker: tx-sender, - unlock-burn-height: unlock-burn-height, - unlock-bytes: unlock-bytes, + signer: signer, + staker: tx-sender, amount-ustx: amount-ustx, + num-cycle: num-cycles, + first-reward-cycle: first-reward-cycle, + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + unlock-cycle: unlock-cycle, + }) + ) +) + +;; A user can: +;; - Change signers +;; - Extend their lock +;; - Increase STX locked +(define-public (stake-update + (signer-manager ) + (old-signer-manager ) + (cycles-to-extend uint) + (amount-increase uint) + (signer-calldata (optional (buff 500))) + ) + (let ( + (signer (contract-of signer-manager)) + (old-signer (contract-of old-signer-manager)) + (current-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKING)) + ;; This is the first cycle where their STX would be unlocked + (prev-unlock-cycle (+ (get first-reward-cycle current-info) + (get num-cycles current-info) + )) + (unlock-cycle (+ prev-unlock-cycle cycles-to-extend)) + (new-lock-amount (+ (get amount-ustx current-info) amount-increase)) + (current-cycle (current-pox-reward-cycle)) + (first-reward-cycle (+ current-cycle u1)) + (num-cycles (- unlock-cycle current-cycle u1)) + ) + ;; Validate that the staker can join this signer + (try! (contract-call? signer-manager validate-stake! tx-sender + first-reward-cycle num-cycles new-lock-amount u0 false + signer-calldata + )) + ;; Validate that `old-signer-manager` matches their current signer + (asserts! (is-eq old-signer (get signer current-info)) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + ;; The signer must have been registered already + (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; lock period must be in acceptable range. + (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) + + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + ;; Must have enough unlocked STX + (asserts! (>= (get unlocked (stx-account tx-sender)) amount-increase) + ERR_INSUFFICIENT_STX + ) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker tx-sender + first-reward-cycle (- prev-unlock-cycle current-cycle u1) false + ) + ;; Allow any errors + ok-val + ok-val + err-val + true + ) + + ;; Remove the staker from all existing cycles + (try! (remove-staker-from-cycles tx-sender (+ u1 current-cycle) + (- prev-unlock-cycle current-cycle u1) true + )) + + (try! (add-staker-to-signer-cycles tx-sender signer (+ u1 current-cycle) + num-cycles new-lock-amount true + )) + + (map-set staker-info tx-sender { + amount-ustx: new-lock-amount, + first-reward-cycle: (get first-reward-cycle current-info), + num-cycles: (+ (get num-cycles current-info) cycles-to-extend), + signer: signer, + }) + + (ok { + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + staker: tx-sender, + signer: signer, + prev-unlock-height: prev-unlock-cycle, unlock-cycle: unlock-cycle, num-cycles: num-cycles, - pool-or-solo-info: pool-or-solo-info, + amount-ustx: new-lock-amount, }) ) ) -(define-public (stake-extend-pooled - (pool-owner ) - (amount-ustx uint) +(define-public (announce-l1-early-exit + (staker principal) + (old-signer-manager ) + ) + (let ( + (old-signer (contract-of old-signer-manager)) + (membership (unwrap! (get-bond-membership staker) ERR_NOT_BOND_PARTICIPANT)) + (bond-index (get bond-index membership)) + (signer (get signer membership)) + (bond (unwrap-panic (get-protocol-bond bond-index))) + (amount-sats (get-staker-shares-staked-for-cycle staker bond-index true signer)) + (current-total-shares (get-total-shares-staked-for-cycle bond-index true)) + (current-shares (get-signer-shares-staked-for-cycle signer bond-index true)) + ) + ;; Only the early unlock admin for this bond period can call this function. + ;; Calling via other contracts is not allowed. + (asserts! + (and (is-eq contract-caller tx-sender) (is-eq contract-caller (get early-unlock-admin bond))) + ERR_UNAUTHORIZED + ) + (asserts! (get is-l1-lock membership) ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK) + (asserts! (is-eq old-signer signer) ERR_INVALID_OLD_SIGNER_MANAGER) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker staker bond-index u1 + true + ) + ok-val ok-val + err-val true + ) + + (crystallize-rewards signer bond-index true) + + (map-set staker-shares-staked-for-cycle { + is-bond: true, + staker: staker, + signer: signer, + index: bond-index, + } + u0 + ) + (map-set signer-shares-staked-for-cycle { + is-bond: true, + signer: signer, + index: bond-index, + } + (- current-shares amount-sats) + ) + (map-set total-shares-staked-for-cycle { + index: bond-index, + is-bond: true, + } + (- current-total-shares amount-sats) + ) + (ok true) + ) +) + +;; As a bond participant with locked sBTC, remove a portion (or all) +;; of your locked sBTC. +(define-public (unstake-sbtc + (signer-manager ) + (amount-to-withdrawal-sats uint) + ) + (let ( + (staker tx-sender) + (membership (unwrap! (map-get? protocol-bond-memberships staker) + ERR_NOT_BOND_PARTICIPANT + )) + (bond-index (get bond-index membership)) + (signer (get signer membership)) + (current-amount-sats (get-staker-shares-staked-for-cycle staker bond-index true signer)) + (current-total-shares (get-total-shares-staked-for-cycle bond-index true)) + (current-shares (get-signer-shares-staked-for-cycle signer bond-index true)) + (current-total-sbtc-staked (get-total-sbtc-staked)) + ;; Cannot withdrawal more than they've staked + (new-amount-sats (try! (if (<= amount-to-withdrawal-sats current-amount-sats) + + (ok (- current-amount-sats amount-to-withdrawal-sats)) + ERR_INVALID_UNSTAKE_SBTC_AMOUNT + ))) + ) + ;; `signer-manager` must match the current signer + (asserts! (is-eq (contract-of signer-manager) signer) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + + ;; Must be an sBTC lock + (asserts! (not (get is-l1-lock membership)) ERR_CANNOT_UNSTAKE_SBTC) + + ;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + ;; Call `signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? signer-manager checkpoint-staker staker bond-index u1 + true + ) + ok-val ok-val + err-val true + ) + + ;; Take a snapshot of the signer's current rewards + (crystallize-rewards signer bond-index true) + + (map-set staker-shares-staked-for-cycle { + is-bond: true, + staker: staker, + signer: signer, + index: bond-index, + } + new-amount-sats + ) + (map-set signer-shares-staked-for-cycle { + is-bond: true, + signer: signer, + index: bond-index, + } + (- current-shares amount-to-withdrawal-sats) + ) + (map-set total-shares-staked-for-cycle { + is-bond: true, + index: bond-index, + } + (- current-total-shares amount-to-withdrawal-sats) + ) + (var-set total-sbtc-staked + (- current-total-sbtc-staked amount-to-withdrawal-sats) + ) + + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" amount-to-withdrawal-sats + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount-to-withdrawal-sats tx-sender staker none + )) + )) + + (ok { + staker: staker, + signer: signer, + new-amount-sats: new-amount-sats, + }) + ) +) + +;; Unstake - set your STX to unlock at the end of the current cycle +(define-public (unstake (old-signer-manager )) + (let ( + (old-signer (contract-of old-signer-manager)) + (current-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKING)) + (first-reward-cycle (get first-reward-cycle current-info)) + ;; This is the first cycle where their STX would be unlocked + (prev-unlock-cycle (+ first-reward-cycle (get num-cycles current-info))) + (current-cycle (current-pox-reward-cycle)) + (unlock-cycle (+ current-cycle u1)) + ) + (asserts! (is-eq old-signer (get signer current-info)) + ERR_INVALID_OLD_SIGNER_MANAGER + ) + ;;;; must be called directly by the tx-sender or by an allowed contract-caller + (try! (check-caller-allowed)) + + ;; do not allow during a prepare phase + (asserts! (not (is-in-prepare-phase current-cycle)) + ERR_UNSTAKE_IN_PREPARE_PHASE + ) + + ;; Call `old-signer-manager`, and allow them to snapshot current + ;; data before updating. Do not throw any errors. + (match (contract-call? old-signer-manager checkpoint-staker tx-sender + (+ current-cycle u1) (- prev-unlock-cycle current-cycle u1) + false + ) + ok-val ok-val + err-val true + ) + + ;; Remove the staker from all existing cycles + (try! (remove-staker-from-cycles tx-sender (+ u1 current-cycle) + (- prev-unlock-cycle current-cycle u1) true + )) + + (map-set staker-info tx-sender { + amount-ustx: (get amount-ustx current-info), + first-reward-cycle: first-reward-cycle, + num-cycles: (- unlock-cycle first-reward-cycle), + signer: old-signer, + }) + + (ok { + staker: tx-sender, + amount-ustx: (get amount-ustx current-info), + first-reward-cycle: first-reward-cycle, + unlock-cycle: unlock-cycle, + unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), + }) + ) +) + +;;;; Remove a staker from a signer for X cycles +(define-private (remove-staker-from-cycles + (staker principal) + (first-reward-cycle uint) (num-cycles uint) - (unlock-bytes (buff 683)) + (is-stx-staking bool) ) - (let ((owner (contract-of pool-owner))) - (asserts! (is-some (get-pool-info owner)) ERR_POOL_NOT_FOUND) - (try! (contract-call? pool-owner validate-stake! tx-sender amount-ustx - num-cycles unlock-bytes + (ok (try! (fold remove-staker-from-signer-for-cycle + ;; panic is ok here because we've already checked `num-cycles` + (unwrap-panic (slice? + (list + u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 + u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31 + u32 u33 u34 u35 u36 u37 u38 u39 u40 u41 u42 u43 u44 u45 u46 + u47 u48 u49 u50 u51 u52 u53 u54 u55 u56 u57 u58 u59 u60 u61 + u62 u63 u64 u65 u66 u67 u68 u69 u70 u71 u72 u73 u74 u75 u76 + u77 u78 u79 u80 u81 u82 u83 u84 u85 u86 u87 u88 u89 u90 u91 + u92 u93 u94 u95 + ) + u0 num-cycles )) - (inner-stake-extend amount-ustx num-cycles unlock-bytes (ok owner)) + (ok { + staker: staker, + first-reward-cycle: first-reward-cycle, + is-stx-staking: is-stx-staking, + }) + ))) +) + +;; For a given (staker, signer, cycle), remove a staker from +;; that signer. If the signer has gone below the minimum amount to +;; be in the signer set, remove them from the signer set. +(define-private (remove-staker-from-signer-for-cycle + (cycle-index uint) + (accumulator-res (response { + staker: principal, + first-reward-cycle: uint, + is-stx-staking: bool, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (staker (get staker accumulator)) + (cycle (+ cycle-index (get first-reward-cycle accumulator))) + (membership (unwrap! + (map-get? staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + }) + ERR_NOT_STAKING + )) + (signer (get signer membership)) + ;; Get the total uSTX delegated (through protocol bonds and STX-only + ;; staking) to this signer. + (cur-delegated-for-signer (get-amount-delegated-for-signer signer cycle)) + ;; uSTX staked for this signer (through STX-only staking) + (cur-staked-for-signer (get-signer-shares-staked-for-cycle signer cycle false)) + ;; Total uSTX staked (through stx-only staking) this cycle + (total-shares-staked (get-total-shares-staked-for-cycle cycle false)) + (amount (get amount-ustx membership)) + (is-stx-staking (get is-stx-staking accumulator)) + (stake-amount (if is-stx-staking + amount + u0 + )) + (new-delegated (- cur-delegated-for-signer amount)) + (is-in-signer-set (is-some (get-staker-set-item-for-cycle signer cycle))) + ) + ;; Crystallize STX-only rewards before mutating anything + (crystallize-rewards signer cycle false) + (if is-in-signer-set + (if (< new-delegated SIGNER_SET_MIN_USTX) + ;; They've crossed back below the threshold - remove from the signer set + ;; and remove from reward calculations. + (begin + (try! (remove-staker-from-set-for-cycle signer cycle)) + (map-set signer-shares-staked-for-cycle { + index: cycle, + signer: signer, + is-bond: false, + } + u0 + ) + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (- total-shares-staked cur-staked-for-signer) + ) + ) + ;; They are in the signer set - update reward calculations + (begin + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (- total-shares-staked stake-amount) + ) + (map-set signer-shares-staked-for-cycle { + index: cycle, + is-bond: false, + signer: signer, + } + (- cur-staked-for-signer stake-amount) + ) + ) + ) + true + ) + ;; Remove this staker from this signer + (map-delete staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + }) + ;; Update amount delegated + (map-set signer-delegated-per-cycle { + cycle: cycle, + signer: signer, + } + new-delegated + ) + ;; Remove amount for staker + (map-delete staker-shares-staked-for-cycle { + index: cycle, + is-bond: false, + staker: staker, + signer: signer, + }) + ;; Update amount staked + (map-set signer-pending-staked-ustx-per-cycle { + signer: signer, + cycle: cycle, + } + (- (get-signer-pending-staked-ustx-per-cycle signer cycle) + stake-amount + )) + ;; Update total amount delegated this cycle + (map-set ustx-delegated-per-cycle cycle + (- (get-ustx-delegated-for-cycle cycle) amount) + ) + (ok accumulator) ) ) -;; #[allow(unnecessary_public)] -(define-public (stake-extend +(define-private (add-staker-to-signer-cycles + (staker principal) + (signer principal) + (first-reward-cycle uint) + (num-cycles uint) (amount-ustx uint) - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), + (is-stx-staking bool) + ) + (ok (try! (fold add-staker-to-signer-for-cycle + ;; panic is ok here because we've already checked `num-cycles` + (unwrap-panic (slice? + (list + u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 u16 + u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 u30 u31 + u32 u33 u34 u35 u36 u37 u38 u39 u40 u41 u42 u43 u44 u45 u46 + u47 u48 u49 u50 u51 u52 u53 u54 u55 u56 u57 u58 u59 u60 u61 + u62 u63 u64 u65 u66 u67 u68 u69 u70 u71 u72 u73 u74 u75 u76 + u77 u78 u79 u80 u81 u82 u83 u84 u85 u86 u87 u88 u89 u90 u91 + u92 u93 u94 u95 + ) + u0 num-cycles + )) + (ok { + staker: staker, + signer: signer, + amount-ustx: amount-ustx, + first-reward-cycle: first-reward-cycle, + is-stx-staking: is-stx-staking, }) - ;; #[allow(unused_binding)] - (signer-sig (optional (buff 65))) - (signer-key (buff 33)) - ;; #[allow(unused_binding)] - (max-amount uint) - ;; #[allow(unused_binding)] - (auth-id uint) - (num-cycles uint) - (unlock-bytes (buff 683)) + ))) +) + +;; For a given (staker, signer, cycle), update signer state for that +;; cycle and lazily add the signer to the signer set if needed. +;; +;; We also update state for the total STX delegated to this signer, +;; along with the total of STX staked in STX-only staking for this signer. +;; +;; If the signer is above the minimum threshold, only then do we update +;; reward calculation state, so that signers below the _delegation_ threshold +;; don't receive rewards. This means it's possible for a signer to have +;; _more_ than the minimum delegated, but _less_ staked from STX-only stakers, +;; but they'll still receive rewards. +(define-private (add-staker-to-signer-for-cycle + (cycle-index uint) + (accumulator-res (response { + signer: principal, + staker: principal, + amount-ustx: uint, + first-reward-cycle: uint, + is-stx-staking: bool, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (cycle (+ cycle-index (get first-reward-cycle accumulator))) + (signer (get signer accumulator)) + ;; Get the total uSTX delegated (through protocol bonds and STX-only + ;; staking) to this signer. + (cur-delegated-for-signer (get-amount-delegated-for-signer signer cycle)) + (amount (get amount-ustx accumulator)) + (stake-amount (if (get is-stx-staking accumulator) + amount + u0 + )) + (staker (get staker accumulator)) + (prev-staked (get-signer-pending-staked-ustx-per-cycle signer cycle)) + (prev-total-shares-staked (get-total-shares-staked-for-cycle cycle false)) + (new-delegated (+ cur-delegated-for-signer amount)) + ) + ;; Crystallize STX-only rewards before mutating anything + (crystallize-rewards signer cycle false) + (if (>= new-delegated SIGNER_SET_MIN_USTX) + (begin + (map-set signer-shares-staked-for-cycle { + index: cycle, + is-bond: false, + signer: signer, + } + (+ prev-staked stake-amount) + ) + (if (< cur-delegated-for-signer SIGNER_SET_MIN_USTX) + ;; They just crossed the threshold - add to signer set and add to reward calculations + (begin + (try! (add-staker-to-set-for-cycle signer cycle)) + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (+ prev-total-shares-staked prev-staked stake-amount) + ) + ) + ;; They're already over the threshold - update the total by just `stake-amount` + (map-set total-shares-staked-for-cycle { + index: cycle, + is-bond: false, + } + (+ prev-total-shares-staked stake-amount) + ) + ) + ) + + ;; not over the min yet + true + ) + ;; Add the staker's membership + (map-set staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + } { + signer: signer, + amount-ustx: amount, + }) + ;; Update the amount delegated + (map-set signer-delegated-per-cycle { + cycle: cycle, + signer: signer, + } + new-delegated + ) + ;; Update the amount staked for this signer + (map-set signer-pending-staked-ustx-per-cycle { + signer: signer, + cycle: cycle, + } + (+ prev-staked stake-amount) + ) + ;; Update the amount staked for this staker + (map-set staker-shares-staked-for-cycle { + staker: staker, + index: cycle, + is-bond: false, + signer: signer, + } + stake-amount + ) + ;; Set the total ustx delegated this cycle + (map-set ustx-delegated-per-cycle cycle + (+ (get-ustx-delegated-for-cycle cycle) amount) + ) + (ok accumulator) ) +) + +(define-private (lock-sbtc (amount uint)) (begin - (try! (validate-signer-key-usage pox-addr (current-pox-reward-cycle) - "stake-extend" num-cycles signer-sig signer-key amount-ustx - max-amount auth-id tx-sender + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount tx-sender current-contract none )) + (var-set total-sbtc-staked (+ (var-get total-sbtc-staked) amount)) + (ok amount) + ) +) - ;; pox-addr must be valid - (try! (check-pox-addr pox-addr)) +;; Verify l1 lockup information for a staker. This asserts that each lockup +;; corresponds to the right timelock script for this staker, and that the lockup +;; occurred on-chain. If everything is valid, this returns the sum of all lockups in sats. +(define-private (verify-l1-lockups + (staker principal) + (bond-index uint) + (lockups { + outputs: (list 10 + { + height: uint, + tx: (buff 100000), + output-index: uint, + header: (buff 80), + leaf-hashes: (list 14 (buff 32)), + tx-count: uint, + tx-index: uint, + amount: uint, + } + ), + unlock-bytes: (buff 683), + }) + ) + (let ( + (bond (unwrap! (get-protocol-bond bond-index) ERR_BOND_NOT_FOUND)) + (expected-timelock-output (construct-lockup-output-script staker + (get-bond-l1-unlock-height bond-index) + (get unlock-bytes lockups) (get early-unlock-signers bond) + )) + (accumulation (try! (fold validate-l1-lockup (get outputs lockups) + (ok { + sum: u0, + expected-script-hash: expected-timelock-output, + }) + ))) + ) + (ok (get sum accumulation)) + ) +) - (inner-stake-extend amount-ustx num-cycles unlock-bytes - (err { - pox-addr: pox-addr, - signer-key: signer-key, +;; Fold function for validating l1 lockup info +(define-private (validate-l1-lockup + (lockup { + height: uint, + tx: (buff 100000), + output-index: uint, + header: (buff 80), + leaf-hashes: (list 14 (buff 32)), + tx-count: uint, + tx-index: uint, + amount: uint, + }) + (accumulator-res (response { + expected-script-hash: (buff 34), + sum: uint, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (block (try! (parse-block-header (get header lockup)))) + (expected-script-hash (get expected-script-hash accumulator)) + (output (try! (get-bitcoin-tx-output? (get tx lockup) (get output-index lockup) + (get amount lockup) expected-script-hash + ))) + (reversed-txid (get txid output)) + (txid (reverse-buff32 reversed-txid)) + ) + (asserts! (verify-block-header (get header lockup) (get height lockup)) + ERR_INVALID_BTC_HEADER + ) + (asserts! (is-eq (get script output) expected-script-hash) + ERR_INVALID_LOCKUP_SCRIPT + ) + ;; verify merkle proof + (asserts! + (or + (is-eq (get merkle-root block) txid) ;; true, if the transaction is the only transaction + (verify-merkle-proof reversed-txid + (reverse-buff32 (get merkle-root block)) + (get tx-index lockup) (get tx-count lockup) + (get leaf-hashes lockup) + ) + ) + ERR_INVALID_MERKLE_PROOF + ) + (ok { + expected-script-hash: (get expected-script-hash accumulator), + sum: (+ (get sum accumulator) (get amount output)), + }) + ) +) + +;;; Reward calculation + +;; Returns the total balance of rewards received by the contract +(define-read-only (get-rewards) + (let ( + (cur-reserve (var-get reserve-balance)) + (total-staked-sbtc (get-total-sbtc-staked)) + (current-balance (unwrap-panic (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + get-balance current-contract + ))) + ) + (- current-balance total-staked-sbtc cur-reserve) + ) +) + +;; Returns the total amount of newly received sBTC rewards +;; since the last rewards computation +(define-read-only (get-new-rewards) + (let ( + (last-accounted-rewards (var-get last-accounted-rewards-only)) + (rewards-balance (get-rewards)) + ) + (- rewards-balance last-accounted-rewards) + ) +) + +(define-public (calculate-rewards (bond-periods (list 6 uint))) + (let ( + (last-calc (var-get last-reward-compute-height)) + (calculation-height (- (distribution-cycle-to-burn-height (current-distribution-cycle)) + u1 + )) + (cur-reserve (var-get reserve-balance)) + (accrued-rewards (get-new-rewards)) + ) + ;; verify that we are able to compute here + (asserts! (> calculation-height last-calc) + ERR_DISTRIBUTION_ALREADY_COMPUTED + ) + + ;; Verify that all active bonds are included + (try! (assert-all-active-bonds-included bond-periods calculation-height)) + + (let ( + (bond-distributions (try! (fold calculate-bond-rewards bond-periods + (ok { + last-bond-stx-value-ratio: none, + available-rewards: accrued-rewards, + last-bond-index: none, + calculation-height: calculation-height, + }) + ))) + (remaining-rewards (get available-rewards bond-distributions)) + (new-reserve (/ (* remaining-rewards RESERVE_RATIO) u10000)) + (stx-staker-rewards (- remaining-rewards new-reserve)) + (stx-cycle (burn-height-to-reward-cycle calculation-height)) + (cycle-staked-ustx (get-total-shares-staked-for-cycle stx-cycle false)) + (current-rewards-per-ustx (get-rewards-per-token-for-cycle stx-cycle false)) + (prev-accounted-rewards (var-get last-accounted-rewards-only)) + (new-rewards-per-ustx (if (is-eq cycle-staked-ustx u0) + ;; if there are no stx staked, we have a problem + u0 + (/ (* stx-staker-rewards PRECISION) cycle-staked-ustx) + )) + (next-rewards-per-ustx (+ current-rewards-per-ustx new-rewards-per-ustx)) + ) + (print { + topic: "calculate-rewards", + bond-periods: bond-periods, + calculation-height: calculation-height, + remaining-rewards: remaining-rewards, + accrued-rewards: accrued-rewards, + stx-staker-rewards: stx-staker-rewards, + stx-cycle: stx-cycle, + cycle-staked-ustx: cycle-staked-ustx, + next-rewards-per-ustx: next-rewards-per-ustx, }) + (var-set reserve-balance (+ cur-reserve new-reserve)) + (var-set last-reward-compute-height calculation-height) + (var-set last-accounted-rewards-only + (+ prev-accounted-rewards (- accrued-rewards new-reserve)) + ) + (map-set rewards-per-token-for-cycle { + index: stx-cycle, + is-bond: false, + } + next-rewards-per-ustx + ) + (ok true) ) ) ) -(define-private (inner-stake-extend - (amount-ustx uint) - (num-cycles uint) - (unlock-bytes (buff 683)) - (pool-or-solo-info (response principal { - pox-addr: { - version: (buff 1), - hashbytes: (buff 32), - }, - signer-key: (buff 33), - })) +(define-private (calculate-bond-rewards + (bond-index uint) + (accumulator-res (response { + ;; Used to ensure that the list of bonds are sorted correctly + last-bond-stx-value-ratio: (optional uint), + ;; Used as a tie-breaker in the case of bonds with the same + ;; stx-value-ratio + last-bond-index: (optional uint), + ;; How much rewards are available to be distributed + available-rewards: uint, + calculation-height: uint, + } + uint + )) ) (let ( - (current-stacker-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKED)) - (prev-unlock-cycle (- - (+ (get first-reward-cycle current-stacker-info) - (get num-cycles current-stacker-info) - ) - u1 + (accumulator (try! accumulator-res)) + (bond (unwrap! (map-get? protocol-bonds bond-index) ERR_BOND_NOT_FOUND)) + (total-sats (get-total-shares-staked-for-cycle bond-index true)) + (available-rewards (get available-rewards accumulator)) + ;; How much sBTC the bond is supposed to earn per calculation, + ;; which is (totalSats * apy) / 48 + (target-yield (/ (/ (* total-sats (get target-rate bond)) u10000) u48)) + ;; If there is enough to cover the target yield, use that. Otherwise, + ;; this bond gets the remaining rewards. + (earned (if (>= available-rewards target-yield) + target-yield + available-rewards )) - (current-cycle (current-pox-reward-cycle)) - (unlock-cycle (+ current-cycle num-cycles)) - (unlock-burn-height (reward-cycle-to-unlock-height unlock-cycle)) - (account-info (stx-account tx-sender)) + (stx-value-ratio (get stx-value-ratio bond)) + (current-rewards-per-token (get-rewards-per-token-for-cycle bond-index true)) + ;; Prevent divide-by-zero + (new-rewards-per-token (if (is-eq total-sats u0) + u0 + (/ (* earned PRECISION) total-sats) + )) + (calculation-height (get calculation-height accumulator)) + (bond-start-height (bond-period-to-burn-height bond-index)) + (bond-end-height (bond-period-to-burn-height (+ bond-index u6))) + ) + ;; Verify that we're paying out bonds in the right order + (match (get last-bond-stx-value-ratio accumulator) + last-ratio + (asserts! + ;; In a tie-breaker, we still want deterministic results. + ;; Thus, enforce that the earlier bond period comes first + (if (is-eq stx-value-ratio last-ratio) + ;; Note that < prevents the same bond period from + ;; being included twice + (> bond-index + (unwrap-panic (get last-bond-index accumulator)) + ) + (<= stx-value-ratio last-ratio) + ) + ERR_INVALID_BOND_PERIOD_ORDERING + ) + ;; When `none`, this is the first bond we're processing + true ) - (asserts! (is-eq prev-unlock-cycle current-cycle) ERR_CANNOT_EXTEND) - (try! (add-staker-to-reward-cycles tx-sender (+ current-cycle u1) num-cycles)) + (map-set rewards-per-token-for-cycle { + is-bond: true, + index: bond-index, + } + (+ current-rewards-per-token new-rewards-per-token) + ) - ;; The caller has locked STX - we need to ensure that their locked + unlocked balance - ;; is sufficient (asserts! - (>= (+ (get locked account-info) (get unlocked account-info)) - amount-ustx + (and + (> calculation-height bond-start-height) + (<= calculation-height bond-end-height) ) - ERR_INSUFFICIENT_FUNDS + ERR_BOND_NOT_ACTIVE ) - ;; amount must be valid - (asserts! (> amount-ustx u0) ERR_INVALID_AMOUNT) - - ;; lock period must be in acceptable range. - (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) - - (map-set staking-state tx-sender { - amount-ustx: amount-ustx, - first-reward-cycle: (+ current-cycle u1), - num-cycles: num-cycles, - unlock-bytes: unlock-bytes, - pool-or-solo-info: pool-or-solo-info, + (print { + topic: "bond-distribution", + bond-index: bond-index, + target-yield: target-yield, + earned: earned, }) (ok { - stacker: tx-sender, - unlock-burn-height: unlock-burn-height, - unlock-bytes: unlock-bytes, - amount-ustx: amount-ustx, - unlock-cycle: unlock-cycle, - num-cycles: num-cycles, - pool-or-solo-info: pool-or-solo-info, + last-bond-stx-value-ratio: (some stx-value-ratio), + last-bond-index: (some bond-index), + available-rewards: (- available-rewards earned), + calculation-height: calculation-height, }) ) ) -(define-public (register-pool - (pool-owner ) - (signer-key (buff 33)) - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), - }) - ;; #[allow(unused_binding)] - (signer-sig (buff 65)) - ;; #[allow(unused_binding)] - (auth-id uint) +;; Get the total amount of rewards earned since the last +;; rewards snapshot. +;; +;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` +(define-read-only (get-earned + (signer principal) + (index uint) + (is-bond bool) ) - (let ((owner (contract-of pool-owner))) - (try! (verify-signer-key-grant tx-sender signer-key pox-addr)) - - (try! (check-pox-addr pox-addr)) + (let ( + (shares (get-signer-shares-staked-for-cycle signer index is-bond)) + (rpt-current (get-rewards-per-token-for-cycle index is-bond)) + (rpt-paid (get-signer-rewards-per-token-paid-for-cycle signer index is-bond)) + (pending (get-signer-pending-rewards-for-cycle signer index is-bond)) + (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) + ) + (+ pending newly-earned) + ) +) - (try! (contract-call? pool-owner validate-management! tx-sender signer-key - pox-addr +(define-public (claim-rewards + (bond-periods (list 6 uint)) + (reward-cycle uint) + ) + (let ( + (signer contract-caller) + (stx-rewards (update-claimable-rewards signer reward-cycle false)) + (bond-rewards (fold update-claimable-bond-rewards bond-periods { + signer: signer, + total: u0, + bond-rewards: (list), + })) + (bond-totals (get total bond-rewards)) + (total-rewards (+ (get earned stx-rewards) bond-totals)) + (prev-accrued-rewards (var-get last-accounted-rewards-only)) + ) + (asserts! (> total-rewards u0) ERR_NO_CLAIMABLE_REWARDS) + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" total-rewards + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer total-rewards tx-sender signer none + )) )) + ;; Update contract reward snapshot to prevent issues in next calculation + (var-set last-accounted-rewards-only + (- prev-accrued-rewards total-rewards) + ) - (map-set pools owner { - signer-key: signer-key, - pox-addr: pox-addr, + (print { + topic: "claim-rewards", + stx-rewards: stx-rewards, + bond-rewards: (get bond-rewards bond-rewards), + bond-totals: bond-totals, + total-rewards: total-rewards, }) (ok { - owner: owner, - signer-key: signer-key, - pox-addr: pox-addr, + stx-rewards: stx-rewards, + bond-rewards: (get bond-rewards bond-rewards), + bond-totals: bond-totals, + total-rewards: total-rewards, }) ) ) -;; Allow a user to update their staked STX amount or pool while they are staked. -(define-public (stake-update-pooled - (pool-owner ) - (amount-ustx-increase uint) +;; For the provided args, calculate the total newly claimable rewards for the signer. +;; Then, update state to reflect this amount as claimed. +;; +;; Returns the newly claimable amount. Does NOT transfer funds out. +(define-private (update-claimable-rewards + (signer principal) + (index uint) + (is-bond bool) ) - (let ( - (owner (contract-of pool-owner)) - (staker-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKED)) + (let ((earned (crystallize-rewards signer index is-bond))) + ;; After crystallization, all earnings live in pending. + ;; Zero out pending since we're about to pay it. + (map-set signer-pending-rewards-for-cycle { + is-bond: is-bond, + index: index, + signer: signer, + } + u0 ) - (asserts! (is-some (get-pool-info owner)) ERR_POOL_NOT_FOUND) - (try! (contract-call? pool-owner validate-stake! tx-sender - (+ (get amount-ustx staker-info) amount-ustx-increase) - (get num-cycles staker-info) (get unlock-bytes staker-info) - )) - (inner-stake-update amount-ustx-increase (ok owner)) + earned ) ) -;; Allow a user to update their staked STX amount, signer key, -;; and/or PoX address while they are staked. -;; -;; #[allow(unnecessary_public)] -(define-public (stake-update - (amount-ustx-increase uint) - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), +(define-private (update-claimable-bond-rewards + (bond-index uint) + (accumulator { + signer: principal, + total: uint, + bond-rewards: (list 6 + { + earned: uint, + bond-index: uint, + rewards-per-token: uint, + } + ), }) - (signer-key (buff 33)) - ;; #[allow(unused_binding)] - (signer-sig (optional (buff 65))) - ;; #[allow(unused_binding)] - (max-amount uint) - ;; #[allow(unused_binding)] - (auth-id uint) ) - (begin - ;; pox-addr must be valid - (try! (check-pox-addr pox-addr)) + (let ((rewards-info (update-claimable-rewards (get signer accumulator) bond-index true))) + { + signer: (get signer accumulator), + total: (+ (get total accumulator) (get earned rewards-info)), + bond-rewards: (concat + (unwrap-panic (as-max-len? (get bond-rewards accumulator) u5)) + (list (merge rewards-info { bond-index: bond-index })) + ), + } + ) +) - (let ( - (stake-update-result (try! (inner-stake-update amount-ustx-increase - (err { - pox-addr: pox-addr, - signer-key: signer-key, - }) - ))) - (cycles-remaining (- (get unlock-cycle stake-update-result) - (current-pox-reward-cycle) - )) - ) - (try! (validate-signer-key-usage pox-addr (current-pox-reward-cycle) - "stake-update" cycles-remaining signer-sig signer-key - amount-ustx-increase max-amount auth-id tx-sender - )) - (ok stake-update-result) +;; Update all earned-but-unclaimed rewards for a signer, and update the snapshot +;; (signer-rewards-per-token-paid) for the signer. +;; +;; This MUST be called before any update to `signer-shares-staked-for-cycle`, +;; because changes to that state will effect rewards calculations. +(define-private (crystallize-rewards + (signer principal) + (index uint) + (is-bond bool) + ) + (let ( + (earned (get-earned signer index is-bond)) + (rewards-per-token (get-rewards-per-token-for-cycle index is-bond)) + ) + (map-set signer-pending-rewards-for-cycle { + is-bond: is-bond, + index: index, + signer: signer, + } + earned + ) + (map-set signer-rewards-per-token-paid-for-cycle { + is-bond: is-bond, + index: index, + signer: signer, + } + rewards-per-token ) + { + earned: earned, + rewards-per-token: rewards-per-token, + } ) ) -(define-private (inner-stake-update - (amount-ustx-increase uint) - (pool-or-solo-info (response principal { - pox-addr: { - version: (buff 1), - hashbytes: (buff 32), - }, - signer-key: (buff 33), - })) +(define-read-only (assert-all-active-bonds-included + (bond-periods (list 6 uint)) + (calculation-height uint) ) (let ( - (current-stacker-info (unwrap! (get-staker-info tx-sender) ERR_NOT_STAKED)) - (new-amount-ustx (+ (get amount-ustx current-stacker-info) amount-ustx-increase)) - (unlock-cycle (- - (+ (get first-reward-cycle current-stacker-info) - (get num-cycles current-stacker-info) - ) - u1 + (calc-cycle (burn-height-to-reward-cycle calculation-height)) + (first-bond-cycle (var-get first-bond-period-cycle)) + (latest-bond-index (if (<= calc-cycle first-bond-cycle) + u0 + (/ (- calc-cycle first-bond-cycle) BOND_GAP_CYCLES) )) ) - ;; assert that the amount of STX to increase is greater than 0 - (asserts! (> amount-ustx-increase u0) ERR_INVALID_AMOUNT) + (try! (fold assert-active-bond-included (list u0 u1 u2 u3 u4 u5) + (ok { + latest-bond-index: latest-bond-index, + calculation-height: calculation-height, + bond-periods: bond-periods, + }) + )) + (ok true) + ) +) - ;; assert that the staker has sufficient STX to increase their stake - (asserts! (>= (stx-get-balance tx-sender) amount-ustx-increase) - ERR_INSUFFICIENT_FUNDS +(define-private (assert-active-bond-included + (offset uint) + (acc-res (response { + latest-bond-index: uint, + calculation-height: uint, + bond-periods: (list 6 uint), + } + uint + )) + ) + (let ( + (acc (try! acc-res)) + (latest-bond-index (get latest-bond-index acc)) ) + (if (> offset latest-bond-index) + (ok acc) + (let ((bond-index (- latest-bond-index offset))) + (if (is-bond-active-at-height bond-index + (get calculation-height acc) + ) + (begin + (asserts! + (get found + (fold match-uint-in-list (get bond-periods acc) { + needle: bond-index, + found: false, + }) + ) + ERR_ACTIVE_BOND_NOT_INCLUDED + ) + (ok acc) + ) + (ok acc) + ) + ) + ) + ) +) - (map-set staking-state tx-sender { - amount-ustx: new-amount-ustx, - first-reward-cycle: (get first-reward-cycle current-stacker-info), - num-cycles: (get num-cycles current-stacker-info), - unlock-bytes: (get unlock-bytes current-stacker-info), - pool-or-solo-info: pool-or-solo-info, - }) - - (ok { - stacker: tx-sender, - unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), - unlock-bytes: (get unlock-bytes current-stacker-info), - amount-ustx: new-amount-ustx, - unlock-cycle: unlock-cycle, - num-cycles: (get num-cycles current-stacker-info), - pool-or-solo-info: pool-or-solo-info, +;; helper to check if a list contains a value +(define-private (match-uint-in-list + (item uint) + (acc { + needle: uint, + found: bool, }) ) + { + needle: (get needle acc), + found: (or (get found acc) (is-eq item (get needle acc))), + } ) +;; TODO: private fn to transfer funds from reserve +;; (define-private (transfer-from-reserve (amount uint) (recipient uint))) + ;;; Signer key authorization functions (define-public (grant-signer-key (signer-key (buff 33)) - (staker principal) - (pox-addr (optional { - version: (buff 1), - hashbytes: (buff 32), - })) + (signer-manager principal) (auth-id uint) (signer-sig (buff 65)) ) @@ -637,7 +1977,7 @@ (asserts! (is-none (map-get? used-signer-key-grants { signer-key: signer-key, - staker: staker, + signer-manager: signer-manager, auth-id: auth-id, })) ERR_SIGNER_KEY_GRANT_USED @@ -647,7 +1987,7 @@ (is-eq (unwrap! (secp256k1-recover? - (get-signer-grant-message-hash staker pox-addr auth-id) + (get-signer-grant-message-hash signer-manager auth-id) signer-sig ) ERR_INVALID_SIGNATURE_RECOVER @@ -660,7 +2000,7 @@ (asserts! (map-insert used-signer-key-grants { signer-key: signer-key, - staker: staker, + signer-manager: signer-manager, auth-id: auth-id, } true @@ -670,9 +2010,9 @@ (map-set signer-key-grants { signer-key: signer-key, - staker: staker, + signer-manager: signer-manager, } - pox-addr + true ) (ok true) @@ -684,7 +2024,7 @@ ;; ;; Returns a boolean indicating whether the signer key grant existed. (define-public (revoke-signer-grant - (staker principal) + (signer-manager principal) (signer-key (buff 33)) ) (begin @@ -700,230 +2040,352 @@ )) tx-sender ) - ERR_NOT_ALLOWED + ERR_UNAUTHORIZED ) (ok (map-delete signer-key-grants { signer-key: signer-key, - staker: staker, + signer-manager: signer-manager, })) ) ) -;; Generate a message hash for validating a signer key. -;; The message hash follows SIP018 for signing structured data. The structured data -;; is the tuple `{ pox-addr: { version, hashbytes }, reward-cycle, auth-id, max-amount, topic, period }`. -;; The domain is [POX_5_SIGNER_DOMAIN]. -(define-read-only (get-signer-key-message-hash - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), - }) - (reward-cycle uint) - (topic (string-ascii 14)) - (period uint) - (max-amount uint) - (auth-id uint) - ) - (sha256 (concat SIP018_MSG_PREFIX - (concat (sha256 (unwrap-panic (to-consensus-buff? POX_5_SIGNER_DOMAIN))) - (sha256 (unwrap-panic (to-consensus-buff? { - pox-addr: pox-addr, - reward-cycle: reward-cycle, - topic: topic, - period: period, - auth-id: auth-id, - max-amount: max-amount, - }))) - ))) -) - ;; Construct the message hash for validating a signer key grant. Unlike [get-signer-key-message-hash], ;; this message hash does not include `max-amount`, `period`, or `reward-cycle`. The topic is always `"grant-authorization"`. ;; The `pox-addr` field is optional. When `none`, it means the signer key can be used for any PoX address. (define-read-only (get-signer-grant-message-hash - (staker principal) - (pox-addr (optional { - version: (buff 1), - hashbytes: (buff 32), - })) + (signer-manager principal) (auth-id uint) ) (sha256 (concat SIP018_MSG_PREFIX (concat (sha256 (unwrap-panic (to-consensus-buff? POX_5_SIGNER_DOMAIN))) (sha256 (unwrap-panic (to-consensus-buff? { topic: "grant-authorization", - staker: staker, - pox-addr: pox-addr, + signer-manager: signer-manager, auth-id: auth-id, }))) ))) ) -;; Verify a signature from the signing key for this specific stacker. -;; See `get-signer-key-message-hash` for details on the message hash. -;; -;; Note that `reward-cycle` corresponds to the _current_ reward cycle, -;; when used with `stack-stx` and `stack-extend`. Both the reward cycle and -;; the lock period are inflexible, which means that the stacker must confirm their transaction -;; during the exact reward cycle and with the exact period that the signature or authorization was -;; generated for. -;; -;; The `amount` field is checked to ensure it is not larger than `max-amount`, which is -;; a field in the authorization. `auth-id` is a random uint to prevent authorization -;; replays. -;; -;; This function does not verify the payload of the authorization. The caller of -;; this function must ensure that the payload (reward cycle, period, topic, and pox-addr) -;; are valid according to the caller function's requirements. -;; -;; When `signer-sig` is present, the public key is recovered from the signature -;; and compared to `signer-key`. If `signer-sig` is `none`, the function verifies that an authorization was previously -;; added for this key. -;; -;; This function checks to ensure that the authorization hasn't been used yet, but it -;; does _not_ store the authorization as used. The function `consume-signer-key-authorization` -;; handles that, and this read-only function is exposed for client-side verification. -(define-read-only (verify-signer-key-sig - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), - }) - (reward-cycle uint) - (topic (string-ascii 14)) - (period uint) - (signer-sig (buff 65)) +(define-read-only (verify-signer-key-grant + (signer-manager principal) (signer-key (buff 33)) - (amount uint) - (max-amount uint) - (auth-id uint) ) - (begin - ;; Validate that amount is less than or equal to `max-amount` - (asserts! (>= max-amount amount) ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH) - (asserts! - (is-none (map-get? used-signer-key-authorizations { - signer-key: signer-key, - reward-cycle: reward-cycle, - topic: topic, - period: period, - pox-addr: pox-addr, - auth-id: auth-id, - max-amount: max-amount, - })) - ERR_SIGNER_AUTH_USED + (ok (asserts! + (is-some (map-get? signer-key-grants { + signer-key: signer-key, + signer-manager: signer-manager, + })) + ERR_SIGNER_KEY_GRANT_NOT_FOUND + )) +) + +;;; Helper functions + +;; What's the burn height at the start of a given bond index? +(define-read-only (bond-period-to-burn-height (bond-index uint)) + (reward-cycle-to-burn-height (bond-period-to-reward-cycle bond-index)) +) + +;; What reward cycle does a bond index start at? +(define-read-only (bond-period-to-reward-cycle (bond-index uint)) + (+ (var-get first-bond-period-cycle) (* bond-index BOND_GAP_CYCLES)) +) + +;; What's the reward cycle number of the burnchain block height? +;; Will runtime-abort if height is less than the first burnchain block (this is intentional) +(define-read-only (burn-height-to-reward-cycle (height uint)) + (/ (- height (var-get first-burnchain-block-height)) + (var-get pox-reward-cycle-length) + ) +) + +;; What's the burn height at the start of a given reward cycle? +(define-read-only (reward-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) + (* cycle (var-get pox-reward-cycle-length)) + ) +) + +;; Get the L1 unlock height for a given reward cycle. +;; This is equal to exactly halfway through the provided cycle. +(define-read-only (reward-cycle-to-unlock-height (cycle uint)) + (+ (reward-cycle-to-burn-height cycle) + (/ (var-get pox-reward-cycle-length) u2) + ) +) + +;; What's the current PoX reward cycle? +(define-read-only (current-pox-reward-cycle) + (burn-height-to-reward-cycle burn-block-height) +) + +;; At a given burn height, what distribution cycle are we in? +;; This is zero-indexed at the first reward-cycle +(define-read-only (burn-height-to-distribution-index (height uint)) + (/ (- height (var-get first-burnchain-block-height)) + (/ (var-get pox-reward-cycle-length) u2) + ) +) + +;; What's the current distribution cycle? +(define-read-only (current-distribution-cycle) + (burn-height-to-distribution-index burn-block-height) +) + +;; The start burn height of a given distribution cycle +(define-read-only (distribution-cycle-to-burn-height (cycle uint)) + (+ (var-get first-burnchain-block-height) + (* cycle (/ (var-get pox-reward-cycle-length) u2)) + ) +) + +;; Are we currently in a prepare phase at the end of `current-cycle`? +(define-read-only (is-in-prepare-phase (current-cycle uint)) + (>= burn-block-height + (- (reward-cycle-to-burn-height (+ current-cycle u1)) + (var-get pox-prepare-cycle-length) + )) +) + +(define-read-only (is-bond-active-at-height + (bond-index uint) + (calculation-height uint) + ) + (let ( + (bond-start-height (bond-period-to-burn-height bond-index)) + (bond-end-height (bond-period-to-burn-height (+ bond-index u6))) ) - (ok (asserts! - (is-eq - (unwrap! - (secp256k1-recover? - (get-signer-key-message-hash pox-addr reward-cycle topic - period max-amount auth-id - ) - signer-sig - ) - ERR_INVALID_SIGNATURE_RECOVER + (and + (is-some (map-get? protocol-bonds bond-index)) + (> calculation-height bond-start-height) + (<= calculation-height bond-end-height) + ) + ) +) + +;; Used for PoX parameters discovery +(define-read-only (get-pox-info) + (ok { + min-amount-ustx: SIGNER_SET_MIN_USTX, + reward-cycle-id: (current-pox-reward-cycle), + prepare-cycle-length: (var-get pox-prepare-cycle-length), + first-burnchain-block-height: (var-get first-burnchain-block-height), + reward-cycle-length: (var-get pox-reward-cycle-length), + total-liquid-supply-ustx: stx-liquid-supply, + }) +) + +(define-read-only (get-bond-allowance + (bond-index uint) + (staker principal) + ) + (map-get? protocol-bond-allowances { + bond-index: bond-index, + staker: staker, + }) +) + +;; Get _current_ bond member info +(define-read-only (get-bond-membership (staker principal)) + (match (map-get? protocol-bond-memberships staker) + membership (if (<= + (+ BOND_LENGTH_CYCLES + (bond-period-to-reward-cycle (get bond-index membership)) ) - signer-key + (current-pox-reward-cycle) ) - ERR_INVALID_SIGNATURE_PUBKEY - )) + none + (some membership) + ) + none ) ) -;; This function does two things: +;; For a given `stx-value-ratio`, which represents "ustx per 100 sats", +;; and a given `min-ustx-ratio`, which represents a minimum amount +;; of STX that must be locked relative to BTC (in basis points), +;; and a given `sats-amount`, calculate the minimum amount +;; of STX needed to hit `min-ustx-ratio`. ;; -;; - Verify that a signer key is authorized to be used -;; - Updates the `used-signer-key-authorizations` map to prevent reuse -;; -;; This "wrapper" method around `verify-signer-key-sig` allows that function to remain -;; read-only, so that it can be used by clients as a sanity check before submitting a transaction. -(define-private (consume-signer-key-authorization - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), - }) - (reward-cycle uint) - (topic (string-ascii 14)) - (period uint) - (signer-sig (buff 65)) - (signer-key (buff 33)) - (amount uint) - (max-amount uint) - (auth-id uint) +;; This is equal to the value-weighted amount of `sats-amount` multiplied +;; by the percentage of `min-ustx-ratio` in STX terms. +(define-read-only (min-ustx-for-sats-amount + (sats-amount uint) + (stx-value-ratio uint) + (min-ustx-ratio uint) ) - (begin - ;; verify the authorization - (try! (verify-signer-key-sig pox-addr reward-cycle topic period signer-sig - signer-key amount max-amount auth-id - )) - ;; update the `used-signer-key-authorizations` map - (asserts! - (map-insert used-signer-key-authorizations { - signer-key: signer-key, - reward-cycle: reward-cycle, - topic: topic, - period: period, - pox-addr: pox-addr, - auth-id: auth-id, - max-amount: max-amount, - } - true + (/ (* (/ (* stx-value-ratio sats-amount) u100) min-ustx-ratio) u10000) +) + +;; Get the _current_ info for a staker. If their +;; stake has expired, this will return `none`. +(define-read-only (get-staker-info (staker principal)) + (match (map-get? staker-info staker) + info + (if (<= (+ (get first-reward-cycle info) (get num-cycles info)) + (current-pox-reward-cycle) ) - ERR_SIGNER_AUTH_USED + ;; present, but lock has expired + none + ;; present, and lock has not expired + (some info) ) - (ok true) + ;; no state at all + none ) ) -;; if signer-sig-opt is present, verify the signature. Otherwise, -;; verify that a grant was previously added for this key. -(define-private (validate-signer-key-usage - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), +(define-read-only (get-signer-info (signer principal)) + (map-get? signers signer) +) + +;; Get the total uSTX delegated (through protocol bonds and STX-only +;; staking) to this signer. +(define-read-only (get-amount-delegated-for-signer + (signer principal) + (cycle uint) + ) + (default-to u0 + (map-get? signer-delegated-per-cycle { + cycle: cycle, + signer: signer, }) - (reward-cycle uint) - (topic (string-ascii 14)) - (period uint) - (signer-sig-opt (optional (buff 65))) - (signer-key (buff 33)) - (amount uint) - (max-amount uint) - (auth-id uint) + ) +) + +;; Get per-cycle staker signer membership info +(define-read-only (get-signer-cycle-membership (staker principal) + (cycle uint) ) - (match signer-sig-opt - signer-sig (consume-signer-key-authorization pox-addr reward-cycle topic period - signer-sig signer-key amount max-amount auth-id - ) - (verify-signer-key-grant staker signer-key pox-addr) + (map-get? staker-signer-cycle-memberships { + staker: staker, + cycle: cycle, + }) +) + +(define-read-only (get-signer-key (staker principal)) + (map-get? signers staker) +) + +(define-read-only (get-total-sbtc-staked-for-bond (bond-index uint)) + (default-to u0 (map-get? protocol-bonds-total-staked bond-index)) +) + +(define-read-only (get-rewards-per-token-for-cycle + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? rewards-per-token-for-cycle { + index: index, + is-bond: is-bond, + }) ) ) -(define-read-only (verify-signer-key-grant +(define-read-only (get-total-shares-staked-for-cycle + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? total-shares-staked-for-cycle { + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-signer-shares-staked-for-cycle + (signer principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? signer-shares-staked-for-cycle { + index: index, + is-bond: is-bond, + signer: signer, + }) + ) +) + +;; Get the amount of shares staked for a given staker in a certain cycle. +(define-read-only (get-staker-shares-staked-for-cycle (staker principal) - (signer-key (buff 33)) - (pox-addr { - version: (buff 1), - hashbytes: (buff 32), + (index uint) + (is-bond bool) + (signer principal) + ) + (default-to u0 + (map-get? staker-shares-staked-for-cycle { + index: index, + staker: staker, + is-bond: is-bond, + signer: signer, }) ) - (ok (asserts! - (match (unwrap! - (map-get? signer-key-grants { - signer-key: signer-key, - staker: staker, - }) - ERR_SIGNER_KEY_GRANT_NOT_FOUND - ) - grant-pox-addr (is-eq grant-pox-addr pox-addr) - true - ) - ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH - )) ) -;;; Validation helpers +(define-read-only (get-signer-rewards-per-token-paid-for-cycle + (signer principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? signer-rewards-per-token-paid-for-cycle { + signer: signer, + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-signer-pending-rewards-for-cycle + (signer principal) + (index uint) + (is-bond bool) + ) + (default-to u0 + (map-get? signer-pending-rewards-for-cycle { + signer: signer, + index: index, + is-bond: is-bond, + }) + ) +) + +(define-read-only (get-signer-pending-staked-ustx-per-cycle + (signer principal) + (cycle uint) + ) + (default-to u0 + (map-get? signer-pending-staked-ustx-per-cycle { + signer: signer, + cycle: cycle, + }) + ) +) + +(define-read-only (get-last-reward-compute-height) + (var-get last-reward-compute-height) +) + +(define-read-only (get-reserve-balance) + (var-get reserve-balance) +) + +(define-read-only (get-total-sbtc-staked) + (var-get total-sbtc-staked) +) + +(define-read-only (get-last-accounted-rewards-only) + (var-get last-accounted-rewards-only) +) + +(define-read-only (get-ustx-delegated-for-cycle (reward-cycle uint)) + (default-to u0 (map-get? ustx-delegated-per-cycle reward-cycle)) +) (define-read-only (check-pox-lock-period (lock-period uint)) (and @@ -932,43 +2394,66 @@ ) ) -(define-read-only (check-pox-addr (pox-addr { - version: (buff 1), - hashbytes: (buff 32), -})) - (let ( - (version (buff-to-uint-be (get version pox-addr))) - (expected-len (if (<= version MAX_ADDRESS_VERSION_BUFF_20) - u20 - u32 - )) - ) - (ok (asserts! - (and - (<= version MAX_ADDRESS_VERSION) - (is-eq (len (get hashbytes pox-addr)) expected-len) - ) - ERR_INVALID_POX_ADDRESS - )) +(define-read-only (get-protocol-bond (bond-index uint)) + (map-get? protocol-bonds bond-index) +) + +;; Returns the expected L1 unlock height for a given bond index. +;; This is equal to 1/2 of a reward cycle before the end of the bond period. +(define-read-only (get-bond-l1-unlock-height (bond-index uint)) + (- (bond-period-to-burn-height (+ bond-index u6)) + (/ (var-get pox-reward-cycle-length) u2) ) ) -;; Is the address mode valid for a PoX address? -(define-read-only (check-pox-addr-version (version (buff 1))) - (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION) +;;; Contract caller allowances + +(define-read-only (check-caller-allowed) + (ok (asserts! + (or + (is-eq tx-sender contract-caller) + (match (unwrap! + (map-get? allowance-contract-callers { + sender: tx-sender, + contract-caller: contract-caller, + }) + ERR_UNAUTHORIZED_CALLER + ) + expiration (< burn-block-height expiration) + true + ) + ) + ERR_UNAUTHORIZED_CALLER + )) ) -;; Is this buffer the right length for the given PoX address? -(define-read-only (check-pox-addr-hashbytes - (version (buff 1)) - (hashbytes (buff 32)) +;; Revoke contract-caller authorization to call stacking methods +(define-public (disallow-contract-caller (caller principal)) + (begin + (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED_CALLER) + (ok (map-delete allowance-contract-callers { + sender: tx-sender, + contract-caller: caller, + })) ) - (if (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION_BUFF_20) - (is-eq (len hashbytes) u20) - (if (<= (buff-to-uint-be version) MAX_ADDRESS_VERSION_BUFF_32) - (is-eq (len hashbytes) u32) - false - ) +) + +;; Give a contract-caller authorization to call stacking methods +;; normally, stacking methods may only be invoked by _direct_ transactions +;; (i.e., the tx-sender issues a direct contract-call to the stacking methods) +;; by issuing an allowance, the tx-sender may call through the allowed contract +(define-public (allow-contract-caller + (caller principal) + (until-burn-ht (optional uint)) + ) + (begin + (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED_CALLER) + (ok (map-set allowance-contract-callers { + sender: tx-sender, + contract-caller: caller, + } + until-burn-ht + )) ) ) @@ -1103,44 +2588,351 @@ ) ) -(define-private (add-staker-to-reward-cycles - (staker principal) - (first-reward-cycle uint) - (num-cycles uint) +(define-private (remove-staker-from-set-for-cycle + (stacker principal) + (cycle uint) ) - (let ((cycle-indexes (unwrap! - (slice? - (list - u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 - u16 u17 u18 u19 u20 u21 u22 u23 + (let ( + (node (unwrap! + (map-get? staker-set-ll-for-cycle { + cycle: cycle, + staker: stacker, + }) + ERR_NOT_STAKING + )) + (prev-item (get prev node)) + (next-item (get next node)) + ) + (match prev-item + prev-stacker + (map-set staker-set-ll-for-cycle { + cycle: cycle, + staker: prev-stacker, + } { + prev: (get prev + (unwrap-panic (map-get? staker-set-ll-for-cycle { + staker: prev-stacker, + cycle: cycle, + })) + ), + next: next-item, + }) + ;; this is the first item + (match next-item + next + (map-set staker-set-ll-first-for-cycle cycle next) + ;; no previous or next - this is the only item + (begin + (map-delete staker-set-ll-last-for-cycle cycle) + (map-delete staker-set-ll-first-for-cycle cycle) ) - u0 num-cycles ) - ERR_INVALID_NUM_CYCLES - ))) - (try! (fold add-staker-to-nth-reward-cycle cycle-indexes - (ok { - staker: staker, - first-reward-cycle: first-reward-cycle, + ) + + (match next-item + next-stacker (map-set staker-set-ll-for-cycle { + cycle: cycle, + staker: next-stacker, + } { + prev: prev-item, + next: (get next + (unwrap-panic (map-get? staker-set-ll-for-cycle { + staker: next-stacker, + cycle: cycle, + })) + ), }) - )) + (match prev-item + prev-stacker + (map-set staker-set-ll-last-for-cycle cycle prev-stacker) + ;; This is the only item - we've already handled this, though + true + ) + ) + (map-delete staker-set-ll-for-cycle { + cycle: cycle, + staker: stacker, + }) (ok true) ) ) -(define-private (add-staker-to-nth-reward-cycle - (cycle-index uint) - (params-resp (response { - staker: principal, - first-reward-cycle: uint, - } - uint +;;;; Clarity-Bitcoin helpers + +;; Parse a Bitcoin block header. +;; Returns a tuple structured as folowed on success: +;; (ok { +;; version: uint, ;; block version, +;; parent: (buff 32), ;; parent block hash, +;; merkle-root: (buff 32), ;; merkle root for all this block's transactions +;; timestamp: uint, ;; UNIX epoch timestamp of this block, in seconds +;; nbits: uint, ;; compact block difficulty representation +;; nonce: uint ;; PoW solution +;; }) +(define-read-only (parse-block-header (headerbuff (buff 80))) + (let ( + (ctx { + txbuff: headerbuff, + index: u0, + }) + (parsed-version (try! (read-uint32 ctx))) + (parsed-parent-hash (try! (read-hashslice (get ctx parsed-version)))) + (parsed-merkle-root (try! (read-hashslice (get ctx parsed-parent-hash)))) + (parsed-timestamp (try! (read-uint32 (get ctx parsed-merkle-root)))) + (parsed-nbits (try! (read-uint32 (get ctx parsed-timestamp)))) + (parsed-nonce (try! (read-uint32 (get ctx parsed-nbits)))) + ) + (ok { + version: (get uint32 parsed-version), + parent: (get hashslice parsed-parent-hash), + merkle-root: (get hashslice parsed-merkle-root), + timestamp: (get uint32 parsed-timestamp), + nbits: (get uint32 parsed-nbits), + nonce: (get uint32 parsed-nonce), + }) + ) +) + +;; Reads the next four bytes from txbuff as a little-endian 32-bit integer, and updates the index. +;; Returns (ok { uint32: uint, ctx: { txbuff: (buff 4096), index: uint } }) on success. +;; Returns ERR_READ_TX_OUT_OF_BOUNDS if we read past the end of txbuff +(define-read-only (read-uint32 (ctx { + txbuff: (buff 4096), + index: uint, +})) + (let ( + (data (get txbuff ctx)) + (base (get index ctx)) + ) + (ok { + uint32: (buff-to-uint-le (unwrap-panic (as-max-len? + (unwrap! (slice? data base (+ base u4)) ERR_READ_TX_OUT_OF_BOUNDS) + u4 + ))), + ctx: { + txbuff: data, + index: (+ u4 base), + }, + }) + ) +) + +;; Reads a little-endian hash -- consume the next 32 bytes, and reverse them. +;; Returns (ok { hashslice: (buff 32), ctx: { txbuff: (buff 4096), index: uint } }) on success, and updates the index. +;; Returns ERR_READ_TX_OUT_OF_BOUNDS if we read past the end of txbuff. +(define-read-only (read-hashslice (old-ctx { + txbuff: (buff 4096), + index: uint, +})) + (let ( + (slice-start (get index old-ctx)) + (target-index (+ u32 slice-start)) + (txbuff (get txbuff old-ctx)) + (hash-le (unwrap-panic (as-max-len? + (unwrap! (slice? txbuff slice-start target-index) + ERR_READ_TX_OUT_OF_BOUNDS + ) + u32 + ))) + ) + (ok { + hashslice: (reverse-buff32 hash-le), + ctx: { + txbuff: txbuff, + index: target-index, + }, + }) + ) +) + +(define-read-only (reverse-buff32 (input (buff 32))) + (unwrap-panic (as-max-len? + (concat + (reverse-buff16 (unwrap-panic (as-max-len? (unwrap-panic (slice? input u16 u32)) u16))) + (reverse-buff16 (unwrap-panic (as-max-len? (unwrap-panic (slice? input u0 u16)) u16))) + ) + u32 + )) +) + +(define-private (reverse-buff16 (input (buff 16))) + (unwrap-panic (slice? (unwrap-panic (to-consensus-buff? (buff-to-uint-le input))) u1 u17)) +) +(define-read-only (get-bc-h-hash (bh uint)) + (get-burn-block-info? header-hash bh) +) + +;; Verify that a block header hashes to a burnchain header hash at a given height. +;; Returns true if so; false if not. +;; +;; TODO: remove the `is-in-mainnet` check, instead use proper mocks +(define-read-only (verify-block-header + (headerbuff (buff 80)) + (expected-block-height uint) + ) + (match (get-burn-block-info? header-hash expected-block-height) + bhh (if is-in-mainnet + (is-eq bhh (reverse-buff32 (sha256 (sha256 headerbuff)))) + true + ) + false + ) +) + +;; Get the txid of a transaction, but little-endian. +;; This is the reverse of what you see on block explorers. +(define-read-only (get-reversed-txid (tx (buff 100000))) + (sha256 (sha256 tx)) +) + +;; TODO: replace with clarity built-ins +(define-private (verify-merkle-proof + ;; #[allow(unused_binding)] + (leaf-hash (buff 32)) + ;; #[allow(unused_binding)] + (root-hash (buff 32)) + ;; #[allow(unused_binding)] + (tx-index uint) + ;; #[allow(unused_binding)] + (tx-count uint) + ;; #[allow(unused_binding)] + (leaf-hashes (list 14 (buff 32))) + ) + true +) + +;; TODO: replace with clarity built-ins +(define-private (get-bitcoin-tx-output? + (tx-bytes (buff 100000)) + ;; #[allow(unused_binding)] + (output-index uint) + ;; TODO: remove when built-in exists + ;; #[allow(unused_binding)] + (amount uint) + ;; TODO: remove when built-in exists + ;; #[allow(unused_binding)] + (script (buff 34)) + ) + (if true + (ok { + amount: amount, + script: script, + txid: (get-reversed-txid tx-bytes), + }) + (err u1) ;; indeterminate type otherwise + ) +) + +;;; Lock script helpers + +;; Contruct an L1 lockup script +(define-read-only (construct-lockup-script + (staker principal) + (unlock-burn-height uint) + (unlock-bytes (buff 683)) + (early-unlock-bytes (buff 683)) + ) + (concat (push-script-bytes (unwrap-panic (to-consensus-buff? staker))) + (concat 0x7563 ;; OP_DROP, OP_IF + (concat (push-c-script-num unlock-burn-height) + (concat 0xb175 ;; OP_CHECKLOCKTIMEVERIFY, OP_DROP + (concat (push-script-bytes unlock-bytes) + (concat 0x67 ;; OP_ELSE + (concat (push-script-bytes early-unlock-bytes) + (concat (push-script-bytes unlock-bytes) 0x68 + ;; OP_ENDIF + )) + )) + )) + )) +) + +;; Construct the p2wsh output script for a L1 lockup address +(define-read-only (construct-lockup-output-script + (staker principal) + (unlock-burn-height uint) + (unlock-bytes (buff 683)) + (early-unlock-bytes (buff 683)) + ) + (concat 0x0020 + (sha256 (construct-lockup-script staker unlock-burn-height unlock-bytes + early-unlock-bytes )) ) - (let ((params (try! params-resp))) - (try! (add-staker-to-set-for-cycle (get staker params) - (+ (get first-reward-cycle params) cycle-index) +) + +;; Convert a u8 or u16 to a little-endian byte buffer, +;; ONLY FOR n < 0xffff +(define-read-only (uint-to-buff-le (n uint)) + (unwrap-panic (as-max-len? + (unwrap-panic (slice? (unwrap-panic (to-consensus-buff? n)) + (if (< n u256) + u16 + u17 + ) u17 )) - (ok params) + u2 + )) +) + +;; Construct the correct script for pushing bytes into a Bitcoin script. +;; +;; If len < 76, just push the length +;; If len < 256, push PUSHDATA1, then the little-endian length +;; If len < 65535 (0xffff), push PUSHDATA2, then the U16LE-encoded length +(define-read-only (push-script-bytes (bytes (buff 1024))) + (let ((byte-length (len bytes))) + (concat + (if (< byte-length u76) + (uint-to-buff-le byte-length) + (if (< byte-length u256) + (concat 0x4c (uint-to-buff-le byte-length)) + (concat 0x4d (uint-to-buff-le byte-length)) + ) + ) + bytes + ) + ) +) + +(define-read-only (serialize-c-script-num (n uint)) + (unwrap-panic (as-max-len? + (if (is-eq n u0) + 0x + (let ( + (bytes (unwrap-panic (to-consensus-buff? n))) + (b0 (unwrap-panic (slice? bytes u16 u17))) + (b1 (unwrap-panic (slice? bytes u15 u16))) + (b2 (unwrap-panic (slice? bytes u14 u15))) + ) + (if (< n u128) + b0 + (if (< n u256) + (concat b0 0x00) + (if (< n u32768) + (concat b0 b1) + (if (< n u65536) + (concat b0 (concat b1 0x00)) + (concat b0 (concat b1 b2)) + ) + ) + ) + ) + ) + ) + u5 + )) +) + +(define-read-only (push-c-script-num (n uint)) + (if (is-eq n u0) + 0x00 + (if (<= n u16) + (unwrap-panic (as-max-len? + (unwrap-panic (slice? (unwrap-panic (to-consensus-buff? (+ u80 n))) u16 u17)) + u1 + )) + (push-script-bytes (serialize-c-script-num n)) + ) ) ) diff --git a/stacking/deployments/default.simnet-plan.yaml b/stacking/deployments/default.simnet-plan.yaml index 6dcc215..d6dccfb 100644 --- a/stacking/deployments/default.simnet-plan.yaml +++ b/stacking/deployments/default.simnet-plan.yaml @@ -65,10 +65,33 @@ genesis: plan: batches: - id: 0 + transactions: + - transaction-type: emulated-contract-publish + contract-name: sbtc-registry + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: sbtc-token + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar + clarity-version: 3 + - transaction-type: emulated-contract-publish + contract-name: sbtc-withdrawal + emulated-sender: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar + clarity-version: 3 + epoch: '3.0' + - id: 1 transactions: - transaction-type: emulated-contract-publish contract-name: pox-5 emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM path: contracts/pox-5.clar clarity-version: 4 + - transaction-type: emulated-contract-publish + contract-name: pox-5-signer + emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM + path: contracts/pox-5-signer.clar + clarity-version: 4 epoch: '3.3' diff --git a/stacking/deployments/sbtc.devnet-plan.yaml b/stacking/deployments/sbtc.devnet-plan.yaml new file mode 100644 index 0000000..f5ffc6d --- /dev/null +++ b/stacking/deployments/sbtc.devnet-plan.yaml @@ -0,0 +1,34 @@ +id: 0 +name: sBTC Devnet deployment +network: devnet +stacks-node: http://localhost:20443 +bitcoin-node: http://devnet:devnet@localhost:18443 +plan: + batches: + - id: 0 + transactions: + - transaction-type: requirement-publish + contract-id: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry + remap-sender: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + remap-principals: + SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + cost: 112090 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar + clarity-version: 3 + - transaction-type: requirement-publish + contract-id: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + remap-sender: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + remap-principals: + SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + cost: 47590 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar + clarity-version: 3 + - transaction-type: requirement-publish + contract-id: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal + remap-sender: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + remap-principals: + SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4: ST1F7QA2MDF17S807EPA36TSS8AMEFY4KA9TVGWXT + cost: 122640 + path: .cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar + clarity-version: 3 + epoch: '3.0' diff --git a/stacking/package.json b/stacking/package.json index 1efdee6..7e26b50 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -12,7 +12,7 @@ "author": "", "license": "ISC", "dependencies": { - "@clarigen/core": "^4.1.3", + "@clarigen/core": "^4.1.5", "@noble/curves": "^2.0.1", "@scure/base": "^1.2.0", "@scure/btc-signer": "^1.5.0", @@ -29,12 +29,13 @@ "pino-pretty": "^10.3.1" }, "devDependencies": { - "@clarigen/cli": "^4.1.3", + "@clarigen/cli": "^4.1.5", "@dotenvx/dotenvx": "^0.26.0", "@stacks/prettier-config": "^0.0.10", "@total-typescript/tsconfig": "^1.0.4", "tsx": "4.7.1", - "typescript": "^6.0.2" + "typescript": "^6.0.2", + "vitest": "^4.1.3" }, "prettier": "@stacks/prettier-config", "resolutions": { diff --git a/stacking/pox-5-helpers.ts b/stacking/pox-5-helpers.ts index fe61798..4245d24 100644 --- a/stacking/pox-5-helpers.ts +++ b/stacking/pox-5-helpers.ts @@ -1,20 +1,27 @@ import * as BTC from '@scure/btc-signer'; import { - Cl, - createAddress, - encodeStructuredDataBytes, - getAddressFromPublicKey, - signWithKey, + Cl, + createAddress, + encodeStructuredDataBytes, + getAddressFromPublicKey, + signWithKey, } from '@stacks/transactions'; import { hex } from '@scure/base'; -import { ClarigenClient, contractFactory, projectErrors, TESTNET_BURN_ADDRESS } from '@clarigen/core'; +import { + ClarigenClient, + contractFactory, + projectErrors, + TESTNET_BURN_ADDRESS, +} from '@clarigen/core'; import { contracts, project } from './clarigen-types.js'; import { sha256 } from '@noble/hashes/sha2.js'; -import { CHAIN_ID, network } from './common.js'; +import { network } from './common.js'; export const clarigenClient = new ClarigenClient(network); export const pox5 = contractFactory(contracts.pox5, `${TESTNET_BURN_ADDRESS}.pox-5`); +export const pox5Signer = (contractAddress: string) => + contractFactory(contracts.pox5Signer, contractAddress); export const errorCodes = projectErrors(project).pox5; @@ -23,7 +30,7 @@ export function toWitnessOutput(script: Uint8Array) { BTC.p2wsh({ type: 'wsh', script, - }), + }) ); } @@ -48,27 +55,17 @@ export function serializeLockupScript({ } export function signSignerKeyGrant({ - staker, - poxAddr, + signerManager, authId, signerSk, }: { - staker: string; - poxAddr: { version: Uint8Array; hashbytes: Uint8Array } | null; + signerManager: string; authId: bigint; signerSk: Uint8Array; }) { const message = Cl.tuple({ - staker: Cl.principal(staker), + 'signer-manager': Cl.principal(signerManager), topic: Cl.stringAscii('grant-authorization'), - 'pox-addr': poxAddr - ? Cl.some( - Cl.tuple({ - version: Cl.buffer(poxAddr.version), - hashbytes: Cl.buffer(poxAddr.hashbytes), - }), - ) - : Cl.none(), 'auth-id': Cl.uint(authId), }); const fullMessage = encodeStructuredDataBytes({ @@ -76,7 +73,7 @@ export function signSignerKeyGrant({ domain: Cl.tuple({ name: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.name), version: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.version), - 'chain-id': Cl.uint(CHAIN_ID), + 'chain-id': Cl.uint(pox5.constants.pOX_5_SIGNER_DOMAIN.chainId), }), }); const data = signWithKey(signerSk, hex.encode(sha256(fullMessage))); diff --git a/stacking/tests/helpers.test.ts b/stacking/tests/helpers.test.ts new file mode 100644 index 0000000..9a114a9 --- /dev/null +++ b/stacking/tests/helpers.test.ts @@ -0,0 +1,6 @@ +import { test, expect } from 'vitest'; +import { burnBlockToRewardCycle } from '../common.js'; + +test('burnBlockToRewardCycle', () => { + expect(burnBlockToRewardCycle(250)).toBe(13); +}); \ No newline at end of file diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index 4cc79ce..7820c2f 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -18,6 +18,7 @@ mine_microblocks = false microblock_frequency = 1000 # mine_microblocks = true # max_microblocks = 10 +pox_5_sbtc_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-token" [miner] first_attempt_time_ms = 180_000 @@ -127,8 +128,8 @@ epoch_name = "3.4" start_height = $STACKS_34_HEIGHT [[burnchain.epochs]] -epoch_name = "3.5" -start_height = $STACKS_35_HEIGHT +epoch_name = "4.0" +start_height = $STACKS_40_HEIGHT [[ustx_balance]] From 01a4713288b1e3fd9aa10e72aea5dbed29faaea9 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Fri, 15 May 2026 06:49:09 -0700 Subject: [PATCH 11/30] fix: nonce issues, add get-current-aggregate-pubkey --- Dockerfile.stacker | 5 +- docker-compose.yml | 16 +- stacking/btc-staker.ts | 58 +-- stacking/clarigen-types.ts | 42 +- stacking/common.ts | 15 +- ...KAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar | 369 ++++++++++++++++++ ...XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar | 160 ++++++++ ...FAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar | 310 +++++++++++++++ stacking/contracts/pox-5.clar | 134 +++---- stacking/monitor.ts | 4 +- 10 files changed, 991 insertions(+), 122 deletions(-) create mode 100644 stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar create mode 100644 stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar create mode 100644 stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar diff --git a/Dockerfile.stacker b/Dockerfile.stacker index 21eecbd..47c2c70 100644 --- a/Dockerfile.stacker +++ b/Dockerfile.stacker @@ -8,7 +8,8 @@ RUN npm i COPY ./stacking/deployments/*.yaml /root/deployments/ COPY ./stacking/contracts/*.clar /root/contracts/ -COPY ./stacking/.cache/requirements/*.clar /root/.cache/requirements/ COPY ./stacking/*.ts /root/ -CMD ["npx", "tsx", "/root/stacking.ts"] \ No newline at end of file +ENV NODE_OPTIONS=--enable-source-maps + +CMD ["./node_modules/.bin/tsx", "/root/stacking.ts"] diff --git a/docker-compose.yml b/docker-compose.yml index 52a403e..8f815c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 01550f7de9a42102ac7049e8708d00de5eca7aa2 # feat/epoch-4-rc + - &STACKS_BLOCKCHAIN_COMMIT 27877974d3c9a0daea20ec7204cb8d7e7aa95e83 # feat/epoch-4-rc - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za @@ -9,8 +9,8 @@ x-common-vars: - &BITCOIN_RPC_PASS btc - &MINE_INTERVAL ${MINE_INTERVAL:-1s} - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-2s} # bitcoin block times in epoch 2.5 - - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-2s} # bitcoin block times in epoch 3 - - &MINE_INTERVAL_EPOCH40 ${MINE_INTERVAL_EPOCH40:-60s} # bitcoin block times in epoch 4 + - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-1s} # bitcoin block times in epoch 3 + - &MINE_INTERVAL_EPOCH40 ${MINE_INTERVAL_EPOCH40:-2s} # bitcoin block times in epoch 4 - &NAKAMOTO_BLOCK_INTERVAL 2 # seconds to wait between issuing stx-transfer transactions (which triggers Nakamoto block production) - &STACKS_20_HEIGHT ${STACKS_20_HEIGHT:-0} - &STACKS_2_05_HEIGHT ${STACKS_2_05_HEIGHT:-102} @@ -326,6 +326,7 @@ services: STACKING_INTERVAL: 2 # interval (seconds) for checking if stacking transactions are needed POST_TX_WAIT: 10 # seconds to wait after a stacking transaction broadcast before continuing the loop SERVICE_NAME: stacker + NODE_OPTIONS: --enable-source-maps depends_on: - stacks-node @@ -350,6 +351,7 @@ services: STACKING_INTERVAL: 2 POST_TX_WAIT: 10 SERVICE_NAME: btc-staker + NODE_OPTIONS: --enable-source-maps BITCOIN_RPC_HOST: bitcoind BITCOIN_RPC_PORT: *BITCOIN_RPC_PORT BITCOIN_RPC_USER: *BITCOIN_RPC_USER @@ -363,7 +365,7 @@ services: - -c - | set -e - exec npx tsx /root/btc-staker.ts + exec ./node_modules/.bin/tsx /root/btc-staker.ts monitor: networks: @@ -385,6 +387,7 @@ services: EXIT_FROM_MONITOR: *EXIT_FROM_MONITOR STACKS_CHAIN_ID: *STACKS_CHAIN_ID SERVICE_NAME: monitor + NODE_OPTIONS: --enable-source-maps depends_on: - stacks-node entrypoint: @@ -392,7 +395,7 @@ services: - -c - | set -e - exec npx tsx /root/monitor.ts + exec ./node_modules/.bin/tsx /root/monitor.ts tx-broadcaster: networks: @@ -412,6 +415,7 @@ services: POX_REWARD_LENGTH: *POX_REWARD_LENGTH STACKS_CHAIN_ID: *STACKS_CHAIN_ID STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 + NODE_OPTIONS: --enable-source-maps depends_on: - stacks-node entrypoint: @@ -419,7 +423,7 @@ services: - -c - | set -e - exec npx tsx /root/tx-broadcaster.ts + exec ./node_modules/.bin/tsx /root/tx-broadcaster.ts postgres: networks: diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 6cbd681..c669cec 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -30,7 +30,7 @@ import { listUnspent, sendToAddress, } from './btc-helpers.js'; -import { signSignerKeyGrant, pox5, pox5Signer } from './pox-5-helpers.js'; +import { signSignerKeyGrant, pox5, pox5Signer, clarigenClient } from './pox-5-helpers.js'; import { readFile } from 'node:fs/promises'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); @@ -64,7 +64,7 @@ async function initBtcWallet() { async function submitStake(account: Account, poxInfo: PoxInfo) { const stakeFnCall = pox5.stake({ startBurnHt: poxInfo.current_burnchain_block_height!, - amountUstx: 1000_000000n, + amountUstx: 100_000_000000n, numCycles: stakingCyclesPox5, signerManager: account.signerManager, signerCalldata: null, @@ -75,6 +75,7 @@ async function submitStake(account: Account, poxInfo: PoxInfo) { senderKey: account.privKey, network, fee: getNextTxFee(), + nonce: (await fetchAccount(account.stxAddress)).nonce, }); const result = await broadcastTransaction({ transaction: tx, @@ -113,6 +114,10 @@ async function submitStakeExtend(account: Account) { transaction: tx, network, }); + if ('reason' in result) { + account.logger.error({ ...result }, `Error extending stake: ${result.reason}`); + throw new Error(`Error extending stake: ${result.reason}`); + } account.logger.info({ txid: result.txid }, 'L2 stake-extend tx broadcast'); return result; } @@ -158,7 +163,6 @@ async function run() { const accountInfos = await Promise.all( accounts.map(async a => { - a.logger.info({ address: a.stxAddress }, 'Getting account status'); const info = await fetchAccount(a.stxAddress); return { ...a, ...info }; }) @@ -209,22 +213,30 @@ async function run() { await waitForTxConfirmed(deployResult.txid); } - const registerSelf = await makeContractCall({ - ...pox5Signer(account.signerManager).registerSelf({ - signerManager: account.signerManager, - signerKey: hex.decode(account.signerPubKey), - authId, - signerSig: signature, - }), - senderKey: account.privKey, - network, - }); - const registerSelfResult = await broadcastTransaction({ - transaction: registerSelf, - network, - }); - account.logger.info({ ...registerSelfResult }, 'Registered self'); - await waitForTxConfirmed(registerSelfResult.txid); + const signerKey = await clarigenClient.ro(pox5.getSignerInfo(account.signerManager)); + + if (!signerKey) { + const registerSelf = await makeContractCall({ + ...pox5Signer(account.signerManager).registerSelf({ + signerManager: account.signerManager, + signerKey: hex.decode(account.signerPubKey), + authId, + signerSig: signature, + }), + nonce: (await fetchAccount(account.stxAddress)).nonce, + senderKey: account.privKey, + network, + }); + const registerSelfResult = await broadcastTransaction({ + transaction: registerSelf, + network, + }); + if ('reason' in registerSelfResult) { + throw new Error(`Error registering signer manager: ${registerSelfResult.reason}`); + } + account.logger.info({ ...registerSelfResult }, 'Registered self'); + await waitForTxConfirmed(registerSelfResult.txid); + } grantedSignerKeys.add(account.signerManager); } @@ -257,22 +269,22 @@ async function run() { continue; } - account.logger.info({ nowCycle, unlockCycle }, 'Staked through next cycle, skipping'); + // account.logger.info({ nowCycle, unlockCycle }, 'Staked through next cycle, skipping'); } await Promise.all(txIdsToWait.map(waitForTxConfirmed)); } async function deploySBTC(account: Account) { const registry = await readFile( - '.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar', + 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar', 'utf8' ); const token = await readFile( - '.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar', + 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar', 'utf8' ); const withdrawal = await readFile( - '.cache/requirements/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar', + 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar', 'utf8' ); diff --git a/stacking/clarigen-types.ts b/stacking/clarigen-types.ts index a39fc84..5b51fb5 100644 --- a/stacking/clarigen-types.ts +++ b/stacking/clarigen-types.ts @@ -4,6 +4,7 @@ import type { TypedAbiArg, TypedAbiFunction, TypedAbiMap, TypedAbiVariable, Resp export const contracts = { pox5: { "functions": { + addSignerToSetForCycle: {"name":"add-signer-to-set-for-cycle","access":"private","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], Response>, addStakerToBond: {"name":"add-staker-to-bond","access":"private","args":[{"name":"staker-item","type":{"tuple":[{"name":"max-sats","type":"uint128"},{"name":"staker","type":"principal"}]}},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"sum-max-sats","type":"uint128"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"sum-max-sats","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[stakerItem: TypedAbiArg<{ "maxSats": number | bigint; "staker": string; @@ -14,7 +15,6 @@ export const contracts = { "bondIndex": bigint; "sumMaxSats": bigint; }, bigint>>, - addStakerToSetForCycle: {"name":"add-staker-to-set-for-cycle","access":"private","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], Response>, addStakerToSignerCycles: {"name":"add-staker-to-signer-cycles","access":"private","args":[{"name":"staker","type":"principal"},{"name":"signer","type":"principal"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"amount-ustx","type":"uint128"},{"name":"is-stx-staking","type":"bool"}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, signer: TypedAbiArg, firstRewardCycle: TypedAbiArg, numCycles: TypedAbiArg, amountUstx: TypedAbiArg, isStxStaking: TypedAbiArg], Response<{ "amountUstx": bigint; "firstRewardCycle": bigint; @@ -77,7 +77,7 @@ export const contracts = { "isStxStaking": boolean; "staker": string; }, bigint>>, - removeStakerFromSetForCycle: {"name":"remove-staker-from-set-for-cycle","access":"private","args":[{"name":"stacker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[stacker: TypedAbiArg, cycle: TypedAbiArg], Response>, + removeStakerFromSetForCycle: {"name":"remove-staker-from-set-for-cycle","access":"private","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], Response>, removeStakerFromSignerForCycle: {"name":"remove-staker-from-signer-for-cycle","access":"private","args":[{"name":"cycle-index","type":"uint128"},{"name":"accumulator-res","type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"first-reward-cycle","type":"uint128"},{"name":"is-stx-staking","type":"bool"},{"name":"staker","type":"principal"}]},"error":"uint128"}}}} as TypedAbiFunction<[cycleIndex: TypedAbiArg, accumulatorRes: TypedAbiArg, getSignerGrantMessageHash: {"name":"get-signer-grant-message-hash","access":"read_only","args":[{"name":"signer-manager","type":"principal"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, authId: TypedAbiArg], Uint8Array>, getSignerInfo: {"name":"get-signer-info","access":"read_only","args":[{"name":"signer","type":"principal"}],"outputs":{"type":{"optional":{"buffer":{"length":33}}}}} as TypedAbiFunction<[signer: TypedAbiArg], Uint8Array | null>, - getSignerKey: {"name":"get-signer-key","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"buffer":{"length":33}}}}} as TypedAbiFunction<[staker: TypedAbiArg], Uint8Array | null>, getSignerPendingRewardsForCycle: {"name":"get-signer-pending-rewards-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, getSignerPendingStakedUstxPerCycle: {"name":"get-signer-pending-staked-ustx-per-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], bigint>, getSignerRewardsPerTokenPaidForCycle: {"name":"get-signer-rewards-per-token-paid-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getSignerSetFirstItemForCycle: {"name":"get-signer-set-first-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, + getSignerSetItemForCycle: {"name":"get-signer-set-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], { + "next": string | null; + "prev": string | null; +} | null>, + getSignerSetLastItemForCycle: {"name":"get-signer-set-last-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, + getSignerSetNextItemForCycle: {"name":"get-signer-set-next-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], string | null>, + getSignerSetPrevItemForCycle: {"name":"get-signer-set-prev-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], string | null>, getSignerSharesStakedForCycle: {"name":"get-signer-shares-staked-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, getStakerInfo: {"name":"get-staker-info","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { "amountUstx": bigint; @@ -289,18 +296,11 @@ export const contracts = { "numCycles": bigint; "signer": string; } | null>, - getStakerSetFirstItemForCycle: {"name":"get-staker-set-first-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, - getStakerSetItemForCycle: {"name":"get-staker-set-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], { - "next": string | null; - "prev": string | null; -} | null>, - getStakerSetLastItemForCycle: {"name":"get-staker-set-last-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, - getStakerSetNextItemForCycle: {"name":"get-staker-set-next-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], string | null>, - getStakerSetPrevItemForCycle: {"name":"get-staker-set-prev-item-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], string | null>, getStakerSharesStakedForCycle: {"name":"get-staker-shares-staked-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg, signer: TypedAbiArg], bigint>, getTotalSbtcStaked: {"name":"get-total-sbtc-staked","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, getTotalSbtcStakedForBond: {"name":"get-total-sbtc-staked-for-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, getTotalSharesStakedForCycle: {"name":"get-total-shares-staked-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getTotalUstxStacked: {"name":"get-total-ustx-stacked","access":"read_only","args":[{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[rewardCycle: TypedAbiArg], bigint>, getUstxDelegatedForCycle: {"name":"get-ustx-delegated-for-cycle","access":"read_only","args":[{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[rewardCycle: TypedAbiArg], bigint>, isBondActiveAtHeight: {"name":"is-bond-active-at-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"},{"name":"calculation-height","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[bondIndex: TypedAbiArg, calculationHeight: TypedAbiArg], boolean>, isInPreparePhase: {"name":"is-in-prepare-phase","access":"read_only","args":[{"name":"current-cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[currentCycle: TypedAbiArg], boolean>, @@ -339,7 +339,7 @@ export const contracts = { rewardCycleToBurnHeight: {"name":"reward-cycle-to-burn-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, rewardCycleToUnlockHeight: {"name":"reward-cycle-to-unlock-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, serializeCScriptNum: {"name":"serialize-c-script-num","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":5}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, - stakerSetContainsForCycle: {"name":"staker-set-contains-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], boolean>, + signerSetContainsForCycle: {"name":"signer-set-contains-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], boolean>, uintToBuffLe: {"name":"uint-to-buff-le","access":"read_only","args":[{"name":"n","type":"uint128"}],"outputs":{"type":{"buffer":{"length":2}}}} as TypedAbiFunction<[n: TypedAbiArg], Uint8Array>, verifyBlockHeader: {"name":"verify-block-header","access":"read_only","args":[{"name":"headerbuff","type":{"buffer":{"length":80}}},{"name":"expected-block-height","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[headerbuff: TypedAbiArg, expectedBlockHeight: TypedAbiArg], boolean>, verifySignerKeyGrant: {"name":"verify-signer-key-grant","access":"read_only","args":[{"name":"signer-manager","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg], Response> @@ -393,6 +393,15 @@ export const contracts = { "isBond": boolean; "signer": string; }, bigint>, + signerSetLlFirstForCycle: {"name":"signer-set-ll-first-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, + signerSetLlForCycle: {"name":"signer-set-ll-for-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"signer","type":"principal"}]},"value":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}} as TypedAbiMap<{ + "cycle": number | bigint; + "signer": string; +}, { + "next": string | null; + "prev": string | null; +}>, + signerSetLlLastForCycle: {"name":"signer-set-ll-last-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, signerSharesStakedForCycle: {"name":"signer-shares-staked-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ "index": number | bigint; "isBond": boolean; @@ -405,15 +414,6 @@ export const contracts = { "numCycles": bigint; "signer": string; }>, - stakerSetLlFirstForCycle: {"name":"staker-set-ll-first-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, - stakerSetLlForCycle: {"name":"staker-set-ll-for-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"staker","type":"principal"}]},"value":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}} as TypedAbiMap<{ - "cycle": number | bigint; - "staker": string; -}, { - "next": string | null; - "prev": string | null; -}>, - stakerSetLlLastForCycle: {"name":"staker-set-ll-last-for-cycle","key":"uint128","value":"principal"} as TypedAbiMap, stakerSharesStakedForCycle: {"name":"staker-shares-staked-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ "index": number | bigint; "isBond": boolean; diff --git a/stacking/common.ts b/stacking/common.ts index d7d6401..952b2e1 100644 --- a/stacking/common.ts +++ b/stacking/common.ts @@ -76,8 +76,8 @@ export async function fetchAccount(stxAddress: string) { unlock_height: number; locked: string; balance: string; + nonce: number; }; - logger.info({ data }, 'Account data'); const locked = deserializeCV(data.locked.slice(2)); const balance = deserializeCV(data.balance.slice(2)); if (locked.type !== ClarityType.Int || balance.type !== ClarityType.Int) { @@ -88,6 +88,7 @@ export async function fetchAccount(stxAddress: string) { unlockHeight: data.unlock_height, lockedAmount: BigInt(locked.value), balance: BigInt(balance.value), + nonce: data.nonce, }; } @@ -146,7 +147,9 @@ export function didCrossPreparePhase(lastBurnHeight: number, newBurnHeight: numb } export async function waitForTxConfirmed(txid: string) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { + const startedAt = Date.now(); + const timeoutMs = 120_000; const interval = setInterval(async () => { const { data: tx, ...rest } = await apiClient.GET(`/extended/v1/tx/{tx_id}`, { params: { @@ -156,12 +159,20 @@ export async function waitForTxConfirmed(txid: string) { }, }); if (!tx) { + if (Date.now() - startedAt > timeoutMs) { + clearInterval(interval); + reject(new Error(`Timed out waiting for tx ${txid}`)); + return; + } logger.warn({ ...rest }, 'Waiting for tx to be confirmed'); return; } if (tx.tx_status !== 'pending') { if (tx.tx_status !== 'success') { logger.error({ ...tx }, 'Tx failed'); + clearInterval(interval); + reject(new Error(`Tx ${txid} failed with status ${tx.tx_status}`)); + return; } clearInterval(interval); resolve(tx); diff --git a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar new file mode 100644 index 0000000..8d4281d --- /dev/null +++ b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar @@ -0,0 +1,369 @@ +;; sBTC Registry contract + +;; Error codes +(define-constant ERR_UNAUTHORIZED (err u400)) +(define-constant ERR_INVALID_REQUEST_ID (err u401)) +(define-constant ERR_AGG_PUBKEY_REPLAY (err u402)) + +;; Protocol contract type +(define-constant governance-role 0x00) +(define-constant deposit-role 0x01) +(define-constant withdrawal-role 0x02) + +;; Variables +(define-data-var last-withdrawal-request-id uint u0) +(define-data-var current-signature-threshold uint u0) +(define-data-var current-signer-set (list 128 (buff 33)) (list)) +(define-data-var current-aggregate-pubkey (buff 33) 0x00) +(define-data-var current-signer-principal principal tx-sender) + +;; Maps +;; Active protocol contracts +(define-map active-protocol-contracts (buff 1) principal) +(map-set active-protocol-contracts governance-role .sbtc-bootstrap-signers) +(map-set active-protocol-contracts deposit-role .sbtc-deposit) +(map-set active-protocol-contracts withdrawal-role .sbtc-withdrawal) +;; Role for active protocol contracts +(define-map active-protocol-roles principal (buff 1)) +(map-set active-protocol-roles .sbtc-bootstrap-signers governance-role) +(map-set active-protocol-roles .sbtc-deposit deposit-role) +(map-set active-protocol-roles .sbtc-withdrawal withdrawal-role) +;; Internal data structure to store withdrawal +;; requests. Requests are associated with a unique +;; request ID. +(define-map withdrawal-requests uint { + ;; Amount of sBTC being withdrawaled (in sats) + amount: uint, + max-fee: uint, + sender: principal, + ;; BTC recipient address in the same format of + ;; pox contracts + recipient: { + version: (buff 1), + hashbytes: (buff 32), + }, + ;; Burn block height where the withdrawal request was + ;; created + block-height: uint, +}) + +;; Data structure to map request-id to status +;; If status is `none`, the request is pending. +;; Otherwise, the boolean value indicates whether +;; the withdrawal was accepted. +(define-map withdrawal-status uint bool) + +;; Data structure to map successful withdrawal requests +;; to their respective sweep transaction. Stores the +;; txid, burn hash, and burn height. +(define-map completed-withdrawal-sweep uint { + sweep-txid: (buff 32), + sweep-burn-hash: (buff 32), + sweep-burn-height: uint, +}) + +;; Internal data structure to store completed +;; deposit requests & avoid replay attacks. +(define-map deposit-status {txid: (buff 32), vout-index: uint} bool) + +;; Data structure to map successful deposit requests +;; to their respective sweep transaction. Stores the +;; txid, burn hash, and burn height. +(define-map completed-deposits {txid: (buff 32), vout-index: uint} + { + amount: uint, + recipient: principal, + sweep-txid: (buff 32), + sweep-burn-hash: (buff 32), + sweep-burn-height: uint, + } +) + +;; Data structure to store aggregate pubkey, +;; stored to avoid replay +(define-map aggregate-pubkeys (buff 33) bool) + +;; Read-only functions +;; Get a withdrawal request by its ID. +;; This function returns the fields of the withdrawal +;; request, along with its status. +(define-read-only (get-withdrawal-request (id uint)) + (match (map-get? withdrawal-requests id) + request (some (merge request { + status: (map-get? withdrawal-status id) + })) + none + ) +) + +;; Get a completed withdrawal sweep data by its request ID. +;; This function returns the fields of the withdrawal-sweeps map. +(define-read-only (get-completed-withdrawal-sweep-data (id uint)) + (map-get? completed-withdrawal-sweep id) +) + +;; Get a completed deposit by its transaction ID & vout index. +;; This function returns the fields of the completed-deposits map. +(define-read-only (get-completed-deposit (txid (buff 32)) (vout-index uint)) + (map-get? completed-deposits {txid: txid, vout-index: vout-index}) +) + +;; Get a completed deposit sweep data by its transaction ID & vout index. +;; This function returns the fields of the completed-deposits map. +(define-read-only (get-deposit-status (txid (buff 32)) (vout-index uint)) + (map-get? deposit-status {txid: txid, vout-index: vout-index}) +) + +;; Get the current signer set. +;; This function returns the current signer set as a list of principals. +(define-read-only (get-current-signer-data) + { + current-signer-set: (var-get current-signer-set), + current-aggregate-pubkey: (var-get current-aggregate-pubkey), + current-signer-principal: (var-get current-signer-principal), + current-signature-threshold: (var-get current-signature-threshold), + } +) + +;; Get the current aggregate pubkey. +;; This function returns the current aggregate pubkey. +(define-read-only (get-current-aggregate-pubkey) + (var-get current-aggregate-pubkey) +) + +;; Get the current signer principal. +;; This function returns the current signer principal. +(define-read-only (get-current-signer-principal) + (var-get current-signer-principal) +) + +(define-read-only (get-current-signer-set) + (var-get current-signer-set) +) + +(define-read-only (get-active-protocol (contract-flag (buff 1))) + (map-get? active-protocol-contracts contract-flag) +) + + +;; Public functions + +;; Store a new withdrawal request. +;; Note that this function can only be called by other sBTC +;; contracts - it cannot be called by users directly. +;; +;; This function does not handle validation or moving the funds. +;; Instead, it is purely for the purpose of storing the request. +;; +;; The function will emit a print event with the topic "withdrawal-create" +;; and the data of the request. +(define-public (create-withdrawal-request + (amount uint) + (max-fee uint) + (sender principal) + (recipient { version: (buff 1), hashbytes: (buff 32) }) + (height uint) + ) + (let + ( + (id (increment-last-withdrawal-request-id)) + ) + (try! (is-protocol-caller withdrawal-role contract-caller)) + ;; #[allow(unchecked_data)] + (map-insert withdrawal-requests id { + amount: amount, + max-fee: max-fee, + sender: sender, + recipient: recipient, + block-height: height, + }) + (print { + topic: "withdrawal-create", + amount: amount, + request-id: id, + sender: sender, + recipient: recipient, + block-height: height, + max-fee: max-fee, + }) + (ok id) + ) +) + +;; Complete withdrawal request by noting the acceptance in the +;; withdrawal-status state map. +;; +;; This function will emit a print event with the topic +;; "withdrawal-accept". +(define-public (complete-withdrawal-accept + (request-id uint) + (bitcoin-txid (buff 32)) + (output-index uint) + (signer-bitmap uint) + (fee uint) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32)) + ) + (begin + (try! (is-protocol-caller withdrawal-role contract-caller)) + ;; Mark the withdrawal as completed + (map-insert withdrawal-status request-id true) + (map-insert completed-withdrawal-sweep request-id { + sweep-txid: sweep-txid, + sweep-burn-hash: burn-hash, + sweep-burn-height: burn-height, + }) + (print { + topic: "withdrawal-accept", + request-id: request-id, + bitcoin-txid: bitcoin-txid, + signer-bitmap: signer-bitmap, + output-index: output-index, + fee: fee, + burn-hash: burn-hash, + burn-height: burn-height, + sweep-txid: sweep-txid, + }) + (ok true) + ) +) + +;; Complete withdrawal request by noting the rejection in the +;; withdrawal-status state map. +;; +;; This function will emit a print event with the topic +;; "withdrawal-reject". +(define-public (complete-withdrawal-reject + (request-id uint) + (signer-bitmap uint) + ) + (begin + (try! (is-protocol-caller withdrawal-role contract-caller)) + ;; Mark the withdrawal as completed + (map-insert withdrawal-status request-id false) + (print { + topic: "withdrawal-reject", + request-id: request-id, + signer-bitmap: signer-bitmap, + }) + (ok true) + ) +) + +;; Store a new insert request. +;; Note that this function can only be called by other sBTC +;; contracts (specifically the current version of the deposit contract) +;; - it cannot be called by users directly. +;; +;; This function does not handle validation or moving the funds. +;; Instead, it is purely for the purpose of storing the completed deposit. +(define-public (complete-deposit + (txid (buff 32)) + (vout-index uint) + (amount uint) + (recipient principal) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32)) + ) + (begin + (try! (is-protocol-caller deposit-role contract-caller)) + (map-insert deposit-status {txid: txid, vout-index: vout-index} true) + (map-insert completed-deposits {txid: txid, vout-index: vout-index} { + amount: amount, + recipient: recipient, + sweep-txid: sweep-txid, + sweep-burn-hash: burn-hash, + sweep-burn-height: burn-height, + }) + (print { + topic: "completed-deposit", + bitcoin-txid: txid, + output-index: vout-index, + amount: amount, + burn-hash: burn-hash, + burn-height: burn-height, + sweep-txid: sweep-txid, + }) + (ok true) + ) +) + +;; Rotate the signer set, multi-sig principal, & aggregate pubkey +;; This function can only be called by the bootstrap-signers contract. +(define-public (rotate-keys + (new-keys (list 128 (buff 33))) + (new-address principal) + (new-aggregate-pubkey (buff 33)) + (new-signature-threshold uint) + ) + (begin + ;; Check that caller is protocol contract + (try! (is-protocol-caller governance-role contract-caller)) + ;; Check that the aggregate pubkey is not already in the map + (asserts! (map-insert aggregate-pubkeys new-aggregate-pubkey true) ERR_AGG_PUBKEY_REPLAY) + ;; Update the current signer set + (var-set current-signer-set new-keys) + ;; Update the current multi-sig address + (var-set current-signer-principal new-address) + ;; Update the current signature threshold + (var-set current-signature-threshold new-signature-threshold) + ;; Update the current aggregate pubkey + (var-set current-aggregate-pubkey new-aggregate-pubkey) + (print { + topic: "key-rotation", + new-keys: new-keys, + new-address: new-address, + new-aggregate-pubkey: new-aggregate-pubkey, + new-signature-threshold: new-signature-threshold + }) + (ok true) + ) +) + +;; Update protocol contract +;; This function can only be called by the active bootstrap-signers contract +(define-public (update-protocol-contract + (contract-type (buff 1)) + (new-contract principal) + ) + (begin + ;; Check that caller is protocol contract + (try! (is-protocol-caller governance-role contract-caller)) + ;; Update the protocol contract + (map-set active-protocol-contracts contract-type new-contract) + ;; Update the protocol role + (map-set active-protocol-roles new-contract contract-type) + (print { + topic: "update-protocol-contract", + contract-type: contract-type, + new-contract: new-contract, + }) + (ok true) + ) +) + +;; Private functions +;; Increment the last withdrawal request ID and +;; return the new value. +(define-private (increment-last-withdrawal-request-id) + (let + ( + (next-value (+ u1 (var-get last-withdrawal-request-id))) + ) + (var-set last-withdrawal-request-id next-value) + next-value + ) +) + +;; Checks whether the contract-caller is a protocol contract +(define-read-only (is-protocol-caller (contract-flag (buff 1)) (contract principal)) + (begin + ;; Check that contract-caller is an protocol contract + (asserts! (is-eq (some contract) (map-get? active-protocol-contracts contract-flag)) ERR_UNAUTHORIZED) + ;; Check that flag matches the contract-caller + (asserts! (is-eq (some contract-flag) (map-get? active-protocol-roles contract)) ERR_UNAUTHORIZED) + (ok true) + ) +) diff --git a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar new file mode 100644 index 0000000..dcbb947 --- /dev/null +++ b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar @@ -0,0 +1,160 @@ +(define-constant ERR_NOT_OWNER (err u4)) ;; `tx-sender` or `contract-caller` tried to move a token it does not own. +(define-constant ERR_TRANSFER_INDEX_PREFIX u1000) + +(define-fungible-token sbtc-token) +(define-fungible-token sbtc-token-locked) + +(define-data-var token-name (string-ascii 32) "sBTC") +(define-data-var token-symbol (string-ascii 10) "sBTC") +(define-data-var token-uri (optional (string-utf8 256)) (some u"https://ipfs.io/ipfs/bafkreibqnozdui4ntgoh3oo437lvhg7qrsccmbzhgumwwjf2smb3eegyqu")) +(define-constant token-decimals u8) + +(define-read-only (get-current-aggregate-pubkey) 0x0204cff1ade0cc7f74d1b5a2b7c7bee653cfb5e6c0dce360795d314c829c4aaf52) + +;; --- Protocol functions + +(define-public (protocol-lock (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (try! (ft-burn? sbtc-token amount owner)) + (ft-mint? sbtc-token-locked amount owner) + ) +) + +(define-public (protocol-unlock (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (try! (ft-burn? sbtc-token-locked amount owner)) + (ft-mint? sbtc-token amount owner) + ) +) + +(define-public (protocol-mint (amount uint) (recipient principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ft-mint? sbtc-token amount recipient) + ) +) + +(define-public (protocol-burn (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ft-burn? sbtc-token amount owner) + ) +) + +(define-public (protocol-burn-locked (amount uint) (owner principal) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ft-burn? sbtc-token-locked amount owner) + ) +) + +(define-public (protocol-set-name (new-name (string-ascii 32)) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (var-set token-name new-name)) + ) +) + +(define-public (protocol-set-symbol (new-symbol (string-ascii 10)) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (var-set token-symbol new-symbol)) + ) +) + +(define-public (protocol-set-token-uri (new-uri (optional (string-utf8 256))) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (var-set token-uri new-uri)) + ) +) + +(define-private (protocol-mint-many-iter (item {amount: uint, recipient: principal})) + (ft-mint? sbtc-token (get amount item) (get recipient item)) +) + +(define-public (protocol-mint-many (recipients (list 200 {amount: uint, recipient: principal})) (contract-flag (buff 1))) + (begin + (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) + (ok (map protocol-mint-many-iter recipients)) + ) +) + +;; --- Public functions +(define-public (transfer-many + (recipients (list 200 { + amount: uint, + sender: principal, + to: principal, + memo: (optional (buff 34)) }))) + (fold transfer-many-iter recipients (ok u0)) +) + +(define-private (transfer-many-iter + (individual-transfer { + amount: uint, + sender: principal, + to: principal, + memo: (optional (buff 34)) }) + (result (response uint uint))) + (match result + index + (begin + (unwrap! + (transfer + (get amount individual-transfer) + (get sender individual-transfer) + (get to individual-transfer) + (get memo individual-transfer)) + (err (+ ERR_TRANSFER_INDEX_PREFIX index))) + (ok (+ index u1)) + ) + err-index + (err err-index) + ) +) + +;; sip-010-trait + +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) ERR_NOT_OWNER) + (try! (ft-transfer? sbtc-token amount sender recipient)) + (match memo to-print (print to-print) 0x) + (ok true) + ) +) + +(define-read-only (get-name) + (ok (var-get token-name)) +) + +(define-read-only (get-symbol) + (ok (var-get token-symbol)) +) + +(define-read-only (get-decimals) + (ok token-decimals) +) + +(define-read-only (get-balance (who principal)) + (ok (+ (ft-get-balance sbtc-token who) (ft-get-balance sbtc-token-locked who))) +) + +(define-read-only (get-balance-available (who principal)) + (ok (ft-get-balance sbtc-token who)) +) + +(define-read-only (get-balance-locked (who principal)) + (ok (ft-get-balance sbtc-token-locked who)) +) + +(define-read-only (get-total-supply) + (ok (+ (ft-get-supply sbtc-token) (ft-get-supply sbtc-token-locked))) +) + +(define-read-only (get-token-uri) + (ok (var-get token-uri)) +) \ No newline at end of file diff --git a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar new file mode 100644 index 0000000..22aaa77 --- /dev/null +++ b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar @@ -0,0 +1,310 @@ +;; Error codes + +;; The `version` part of the recipient address is invalid +(define-constant ERR_INVALID_ADDR_VERSION (err u500)) +;; The `hashbytes` part of the recipient address is invalid +(define-constant ERR_INVALID_ADDR_HASHBYTES (err u501)) +;; The size of the withdrawal is smaller than the dust limit +(define-constant ERR_DUST_LIMIT (err u502)) +;; The request id was invalid / returned 'none' +(define-constant ERR_INVALID_REQUEST (err u503)) +;; The caller is not the currently-governing multisig principal +(define-constant ERR_INVALID_CALLER (err u504)) +;; The withdrawal request was already processed +(define-constant ERR_ALREADY_PROCESSED (err u505)) +;; The paid fee was higher than requested +(define-constant ERR_FEE_TOO_HIGH (err u506)) +;; The returned index marks the failed transaction in list +(define-constant ERR_WITHDRAWAL_INDEX_PREFIX (unwrap-err! ERR_WITHDRAWAL_INDEX (err true))) +(define-constant ERR_WITHDRAWAL_INDEX (err u507)) +(define-constant ERR_INVALID_BURN_HASH (err u508)) + +;; Maximum value of an address version as a uint +(define-constant MAX_ADDRESS_VERSION u6) +;; Maximum value of an address version that has a 20-byte hashbytes +;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4) +;; Maximum value of an address version that has a 32-byte hashbytes +;; (0x05 and 0x06 have 32-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_32 u6) +;; The minimum amount of sBTC you can withdraw +(define-constant DUST_LIMIT u546) +;; protocol contract type +(define-constant withdraw-role 0x02) + +;; Initiate a new withdrawal request. +;; +;; # Notes +;; +;; ## Amounts +;; +;; This function locks up `amount + max-fee` from the tx-sender's account, +;; and when the withdrawal request is accepted, the signers will send +;; `amount` of sats to the recipient and spend an a fee amount to bitcoin +;; miners where fee less than or equal to max-fee. If fee is less than +;; max-fee, then the difference will be minted back to the user when +;; `accept-withdrawal-request` is invoked. +;; +;; ## The recipient +;; +;; This constraints and meaning of the recipient field is summarized as: +;; ```text +;; version == 0x00 and (len hashbytes) == 20 => P2PKH +;; version == 0x01 and (len hashbytes) == 20 => P2SH +;; version == 0x02 and (len hashbytes) == 20 => P2SH-P2WPKH +;; version == 0x03 and (len hashbytes) == 20 => P2SH-P2WSH +;; version == 0x04 and (len hashbytes) == 20 => P2WPKH +;; version == 0x05 and (len hashbytes) == 32 => P2WSH +;; version == 0x06 and (len hashbytes) == 32 => P2TR +;; ``` +;; Also see +;; +;; Below is a detailed breakdown of bitcoin address types and how they map +;; to the clarity value. In what follows below, the network used for the +;; human-readable parts is inherited from the network of the underlying +;; transaction itself (basically, on stacks mainnet we send to mainnet +;; bitcoin addresses and similarly on stacks testnet we send to bitcoin +;; testnet addresses). +;; +;; ### P2PKH +;; +;; Generally speaking, Pay-to-Public-Key-Hash addresses are formed by +;; taking the Hash160 of the public key, prefixing it with one byte (0x00 +;; on mainnet and 0x6F on testing) and then base58 encoding the result. +;; +;; To specify this address type as the recipient, the `version` is 0x00 and +;; the `hashbytes` is the Hash160 of the public key. +;; +;; +;; ### P2SH, P2SH-P2WPKH, and P2SH-P2WSH +;; +;; Pay-to-script-hash-* addresses are formed by taking the Hash160 of the +;; locking script, prefixing it with one byte (0x05 on mainnet and 0xC4 on +;; testnet) and base58 encoding the result. The difference between them +;; lies with the locking script. For P2SH-P2WPKH addresses, the locking +;; script is: +;; ```text +;; 0 || +;; ``` +;; For P2SH-P2WSH addresses, the locking script is: +;; ```text +;; 0 || +;; ``` +;; And for P2SH addresses you get to choose the locking script in its +;; entirety. +;; +;; Again, after you construct the locking script you take its Hash160, +;; prefix it with one byte and base58 encode it to form the address. To +;; specify these address types in the recipient, the `version` is 0x01, +;; 0x02, and 0x03 (for P2SH, P2SH-P2WPKH, and P2SH-P2WSH respectively) with +;; the `hashbytes` is the Hash160 of the locking script. +;; +;; +;; ### P2WPKH +;; +;; Pay-to-witness-public-key-hash addresses are formed by creating a +;; witness program made entirely of the Hash160 of the compressed public +;; key. +;; +;; To specify this address type in the recipient, the `version` is 0x04 and +;; the `hashbytes` is the Hash160 of the compressed public key. +;; +;; +;; ### P2WSH +;; +;; Pay-to-witness-script-hash addresses are formed by taking a witness +;; program that is compressed entirely of the SHA256 of the redeem script. +;; +;; To specify this address type in the recipient, the `version` is 0x05 and +;; the `hashbytes` is the SHA256 of the redeem script. +;; +;; +;; ### P2TR +;; +;; Pay-to-taproot addresses are formed by "tweaking" the x-coordinate of a +;; public key with a merkle tree. The result of the tweak is used as the +;; witness program for the address. +;; +;; To specify this address type in the recipient, the `version` is 0x06 and +;; the `hashbytes` is the "tweaked" public key. +(define-public (initiate-withdrawal-request (amount uint) + (recipient { version: (buff 1), hashbytes: (buff 32) }) + (max-fee uint) + ) + (begin + (try! (contract-call? .sbtc-token protocol-lock (+ amount max-fee) tx-sender withdraw-role)) + (asserts! (> amount DUST_LIMIT) ERR_DUST_LIMIT) + + ;; Validate the recipient address + (try! (validate-recipient recipient)) + + (ok (try! (contract-call? .sbtc-registry create-withdrawal-request amount max-fee tx-sender recipient burn-block-height))) + ) +) + +;; Accept a withdrawal request +(define-public (accept-withdrawal-request (request-id uint) + (bitcoin-txid (buff 32)) + (signer-bitmap uint) + (output-index uint) + (fee uint) + (burn-hash (buff 32)) + (burn-height uint) + (sweep-txid (buff 32))) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + (request (unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST)) + (requested-max-fee (get max-fee request)) + (requested-amount (get amount request)) + (requester (get sender request)) + ) + + ;; Verify that Bitcoin hasn't forked by comparing the burn hash provided + (asserts! (is-eq (some burn-hash) (get-burn-header burn-height)) ERR_INVALID_BURN_HASH) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + ;; Check whether it was already accepted or rejected + (asserts! (is-none (get status request)) ERR_ALREADY_PROCESSED) + + ;; Check that fee is not higher than requesters max fee + (asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH) + + ;; Burn the locked-sbtc + (try! (contract-call? .sbtc-token protocol-burn-locked (+ requested-amount requested-max-fee) requester withdraw-role)) + + ;; Mint the difference b/w max-fee of the request & fee actually paid back to the user in sBTC + (if (is-eq (- requested-max-fee fee) u0) + true + (try! (contract-call? .sbtc-token protocol-mint (- requested-max-fee fee) requester withdraw-role)) + ) + + ;; Call into registry to confirm accepted withdrawal + (try! (contract-call? .sbtc-registry complete-withdrawal-accept request-id bitcoin-txid output-index signer-bitmap fee burn-hash burn-height sweep-txid)) + + (ok true) + ) +) + +;; Reject a withdrawal request +(define-public (reject-withdrawal-request (request-id uint) (signer-bitmap uint)) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + (withdrawal (unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST)) + (requested-max-fee (get max-fee withdrawal)) + (requested-amount (get amount withdrawal)) + (requester (get sender withdrawal)) + ) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + ;; Check that request status is currently-pending + (asserts! (is-none (get status withdrawal)) ERR_ALREADY_PROCESSED) + + ;; Burn sbtc-locked & re-mint sbtc to original requester + (try! (contract-call? .sbtc-token protocol-unlock (+ requested-amount requested-max-fee) requester withdraw-role)) + + ;; Call into registry to confirm accepted withdrawal + (try! (contract-call? .sbtc-registry complete-withdrawal-reject request-id signer-bitmap)) + + (ok true) + ) +) +;; Complete multiple withdrawal requests +(define-public (complete-withdrawals (withdrawals (list 600 + {request-id: uint, + status: bool, + signer-bitmap: uint, + bitcoin-txid: (optional (buff 32)), + output-index: (optional uint), + fee: (optional uint), + burn-hash: (buff 32), + burn-height: uint, + sweep-txid: (optional (buff 32))}))) + (let + ( + (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) + ) + + ;; Check that the caller is the current signer principal + (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) + + (fold complete-individual-withdrawal-helper withdrawals (ok u0)) + ) +) + +(define-private (complete-individual-withdrawal-helper (withdrawal + {request-id: uint, + status: bool, + signer-bitmap: uint, + bitcoin-txid: (optional (buff 32)), + output-index: (optional uint), + fee: (optional uint), + burn-hash: (buff 32), + burn-height: uint, + sweep-txid: (optional (buff 32))}) + (helper-response (response uint uint))) + (match helper-response + index + (let + ( + (current-request-id (get request-id withdrawal)) + (current-signer-bitmap (get signer-bitmap withdrawal)) + (current-bitcoin-txid (get bitcoin-txid withdrawal)) + (current-output-index (get output-index withdrawal)) + (current-fee (get fee withdrawal)) + ) + (if (get status withdrawal) + ;; accepted + (begin + (asserts! + (and (is-some current-bitcoin-txid) (is-some current-output-index) (is-some current-fee)) + (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) + (unwrap! (accept-withdrawal-request current-request-id (unwrap-panic current-bitcoin-txid) current-signer-bitmap (unwrap-panic current-output-index) (unwrap-panic current-fee) (get burn-hash withdrawal) (get burn-height withdrawal) (unwrap-panic (get sweep-txid withdrawal))) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) + ) + ;; rejected + (unwrap! (reject-withdrawal-request current-request-id current-signer-bitmap) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) + ) + (ok (+ index u1)) + ) + err-response + (err err-response) + ) +) + +;; Validation methods + +;; Validate that a withdrawal's recipient address is well-formed. The logic +;; here follows the same rules as pox-4. +;; +;; At a high-level, the version must be a uint between 0 and 6 (inclusive), +;; and the length of the hashbytes must be 20 bytes if the version is <= 4, +;; and 32 bytes if the version is 5 or 6. +(define-read-only (validate-recipient (recipient { version: (buff 1), hashbytes: (buff 32) })) + (let + ( + (version (get version recipient)) + (hashbytes (get hashbytes recipient)) + (version-int (buff-to-uint-be version)) + ) + ;; Validate the `version` + (asserts! (<= version-int MAX_ADDRESS_VERSION) ERR_INVALID_ADDR_VERSION) + ;; Validate the length of `hashbytes` + (asserts! (if (<= version-int MAX_ADDRESS_VERSION_BUFF_20) + ;; If version is <= 4, then hashbytes must be 20 bytes + (is-eq (len hashbytes) u20) + ;; Otherwise, hashbytes must be 32 bytes + (is-eq (len hashbytes) u32)) + ERR_INVALID_ADDR_HASHBYTES) + (ok true) + ) +) + +;; Return the bitcoin header hash of the bitcoin block at the given height. +(define-read-only (get-burn-header (height uint)) + (get-burn-block-info? header-hash height) +) diff --git a/stacking/contracts/pox-5.clar b/stacking/contracts/pox-5.clar index 4324dd0..fb35809 100644 --- a/stacking/contracts/pox-5.clar +++ b/stacking/contracts/pox-5.clar @@ -1225,7 +1225,7 @@ u0 )) (new-delegated (- cur-delegated-for-signer amount)) - (is-in-signer-set (is-some (get-staker-set-item-for-cycle signer cycle))) + (is-in-signer-set (is-some (get-signer-set-item-for-cycle signer cycle))) ) ;; Crystallize STX-only rewards before mutating anything (crystallize-rewards signer cycle false) @@ -1389,7 +1389,7 @@ (if (< cur-delegated-for-signer SIGNER_SET_MIN_USTX) ;; They just crossed the threshold - add to signer set and add to reward calculations (begin - (try! (add-staker-to-set-for-cycle signer cycle)) + (try! (add-signer-to-set-for-cycle signer cycle)) (map-set total-shares-staked-for-cycle { index: cycle, is-bond: false, @@ -2264,10 +2264,6 @@ }) ) -(define-read-only (get-signer-key (staker principal)) - (map-get? signers staker) -) - (define-read-only (get-total-sbtc-staked-for-bond (bond-index uint)) (default-to u0 (map-get? protocol-bonds-total-staked bond-index)) ) @@ -2387,6 +2383,12 @@ (default-to u0 (map-get? ustx-delegated-per-cycle reward-cycle)) ) +;; How many uSTX are staked? - Required to be named this to +;; work with `chainstate.get_total_ustx_stacked` +(define-read-only (get-total-ustx-stacked (reward-cycle uint)) + (get-ustx-delegated-for-cycle reward-cycle) +) + (define-read-only (check-pox-lock-period (lock-period uint)) (and (>= lock-period u1) @@ -2460,21 +2462,21 @@ ;;; Cycle-based Linked List functions ;; First item in the linked list of stakers -(define-map staker-set-ll-first-for-cycle +(define-map signer-set-ll-first-for-cycle uint principal ) ;; Last item in the linked list of stakers -(define-map staker-set-ll-last-for-cycle +(define-map signer-set-ll-last-for-cycle uint principal ) ;; Linked list of all stakers for a cycle -(define-map staker-set-ll-for-cycle +(define-map signer-set-ll-for-cycle { cycle: uint, - staker: principal, + signer: principal, } { prev: (optional principal), @@ -2482,121 +2484,121 @@ } ) -(define-read-only (get-staker-set-last-item-for-cycle (cycle uint)) - (map-get? staker-set-ll-last-for-cycle cycle) +(define-read-only (get-signer-set-last-item-for-cycle (cycle uint)) + (map-get? signer-set-ll-last-for-cycle cycle) ) -(define-read-only (get-staker-set-first-item-for-cycle (cycle uint)) - (map-get? staker-set-ll-first-for-cycle cycle) +(define-read-only (get-signer-set-first-item-for-cycle (cycle uint)) + (map-get? signer-set-ll-first-for-cycle cycle) ) -(define-read-only (get-staker-set-item-for-cycle - (staker principal) +(define-read-only (get-signer-set-item-for-cycle + (signer principal) (cycle uint) ) - (map-get? staker-set-ll-for-cycle { + (map-get? signer-set-ll-for-cycle { cycle: cycle, - staker: staker, + signer: signer, }) ) -(define-read-only (get-staker-set-next-item-for-cycle - (staker principal) +(define-read-only (get-signer-set-next-item-for-cycle + (signer principal) (cycle uint) ) - (match (map-get? staker-set-ll-for-cycle { + (match (map-get? signer-set-ll-for-cycle { cycle: cycle, - staker: staker, + signer: signer, }) item (get next item) none ) ) -(define-read-only (get-staker-set-prev-item-for-cycle - (staker principal) +(define-read-only (get-signer-set-prev-item-for-cycle + (signer principal) (cycle uint) ) - (match (map-get? staker-set-ll-for-cycle { + (match (map-get? signer-set-ll-for-cycle { cycle: cycle, - staker: staker, + signer: signer, }) item (get prev item) none ) ) -(define-read-only (staker-set-contains-for-cycle - (staker principal) +(define-read-only (signer-set-contains-for-cycle + (signer principal) (cycle uint) ) - (is-some (map-get? staker-set-ll-for-cycle { + (is-some (map-get? signer-set-ll-for-cycle { cycle: cycle, - staker: staker, + signer: signer, })) ) -(define-private (add-staker-to-set-for-cycle - (staker principal) +(define-private (add-signer-to-set-for-cycle + (signer principal) (cycle uint) ) - (let ((last-item (map-get? staker-set-ll-last-for-cycle cycle))) + (let ((last-item (map-get? signer-set-ll-last-for-cycle cycle))) ;; Todo: remove this and guard in a higher-level fn (asserts! - (not (is-some (map-get? staker-set-ll-for-cycle { + (not (is-some (map-get? signer-set-ll-for-cycle { cycle: cycle, - staker: staker, + signer: signer, }))) ERR_ALREADY_STAKED ) (match last-item - last-stacker (let ((last-node (unwrap-panic (map-get? staker-set-ll-for-cycle { + last-signer (let ((last-node (unwrap-panic (map-get? signer-set-ll-for-cycle { cycle: cycle, - staker: last-stacker, + signer: last-signer, })))) - (map-set staker-set-ll-for-cycle { + (map-set signer-set-ll-for-cycle { cycle: cycle, - staker: last-stacker, + signer: last-signer, } { prev: (get prev last-node), - next: (some staker), + next: (some signer), }) - (map-set staker-set-ll-for-cycle { + (map-set signer-set-ll-for-cycle { cycle: cycle, - staker: staker, + signer: signer, } { - prev: (some last-stacker), + prev: (some last-signer), next: none, }) ) (begin ;; This is the first item - (map-set staker-set-ll-for-cycle { + (map-set signer-set-ll-for-cycle { cycle: cycle, - staker: staker, + signer: signer, } { prev: none, next: none, }) - (map-set staker-set-ll-first-for-cycle cycle staker) + (map-set signer-set-ll-first-for-cycle cycle signer) ) ) - (map-set staker-set-ll-last-for-cycle cycle staker) + (map-set signer-set-ll-last-for-cycle cycle signer) (ok true) ) ) (define-private (remove-staker-from-set-for-cycle - (stacker principal) + (signer principal) (cycle uint) ) (let ( (node (unwrap! - (map-get? staker-set-ll-for-cycle { + (map-get? signer-set-ll-for-cycle { cycle: cycle, - staker: stacker, + signer: signer, }) ERR_NOT_STAKING )) @@ -2604,14 +2606,14 @@ (next-item (get next node)) ) (match prev-item - prev-stacker - (map-set staker-set-ll-for-cycle { + prev-signer + (map-set signer-set-ll-for-cycle { cycle: cycle, - staker: prev-stacker, + signer: prev-signer, } { prev: (get prev - (unwrap-panic (map-get? staker-set-ll-for-cycle { - staker: prev-stacker, + (unwrap-panic (map-get? signer-set-ll-for-cycle { + signer: prev-signer, cycle: cycle, })) ), @@ -2620,38 +2622,38 @@ ;; this is the first item (match next-item next - (map-set staker-set-ll-first-for-cycle cycle next) + (map-set signer-set-ll-first-for-cycle cycle next) ;; no previous or next - this is the only item (begin - (map-delete staker-set-ll-last-for-cycle cycle) - (map-delete staker-set-ll-first-for-cycle cycle) + (map-delete signer-set-ll-last-for-cycle cycle) + (map-delete signer-set-ll-first-for-cycle cycle) ) ) ) (match next-item - next-stacker (map-set staker-set-ll-for-cycle { + next-signer (map-set signer-set-ll-for-cycle { cycle: cycle, - staker: next-stacker, + signer: next-signer, } { prev: prev-item, next: (get next - (unwrap-panic (map-get? staker-set-ll-for-cycle { - staker: next-stacker, + (unwrap-panic (map-get? signer-set-ll-for-cycle { + signer: next-signer, cycle: cycle, })) ), }) (match prev-item - prev-stacker - (map-set staker-set-ll-last-for-cycle cycle prev-stacker) + prev-signer + (map-set signer-set-ll-last-for-cycle cycle prev-signer) ;; This is the only item - we've already handled this, though true ) ) - (map-delete staker-set-ll-for-cycle { + (map-delete signer-set-ll-for-cycle { cycle: cycle, - staker: stacker, + signer: signer, }) (ok true) ) diff --git a/stacking/monitor.ts b/stacking/monitor.ts index fbdd2ae..3ea281a 100644 --- a/stacking/monitor.ts +++ b/stacking/monitor.ts @@ -110,8 +110,8 @@ async function loop() { let showPrepareMsg = false; let showCycleMsg = false; let showStxBlockMsg = false; - let burnHeightDate = new Date(blockInfo?.burn_block_time ?? 0 * 1000); - let burnHeightTimeAgo = (new Date().getTime() - burnHeightDate.getTime()) / 1000; + const burnBlockTimeMs = (blockInfo?.burn_block_time ?? 0) * 1000; + const burnHeightTimeAgo = (Date.now() - burnBlockTimeMs) / 1000; const loopLog = logger.child({ height, burnHeight: current_burnchain_block_height, From 3315e8083c67b921edf217ae45ef231e7b3e2e31 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:22:08 -0700 Subject: [PATCH 12/30] feat: update stacks-core image --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8f815c5..b4fbbc3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 27877974d3c9a0daea20ec7204cb8d7e7aa95e83 # feat/epoch-4-rc + - &STACKS_BLOCKCHAIN_COMMIT 87e89f4bbdedbe5d47f58e57591f162ad3853903 # feat/epoch-4-rc - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za From 6a30570a79199d4a7a1b91b247543b3925d88e37 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:23:22 -0700 Subject: [PATCH 13/30] feat: update stacks-core commit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b4fbbc3..3ae4fea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 87e89f4bbdedbe5d47f58e57591f162ad3853903 # feat/epoch-4-rc + - &STACKS_BLOCKCHAIN_COMMIT 84314f08b7b046d113696a0406d081b55ec74e28gs # feat/epoch-4-rc - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za From 141767da24f3330e98d07fe339ae8b8ea32307da Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:31:09 -0700 Subject: [PATCH 14/30] fix: typo in commit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3ae4fea..fecd67c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 84314f08b7b046d113696a0406d081b55ec74e28gs # feat/epoch-4-rc + - &STACKS_BLOCKCHAIN_COMMIT 84314f08b7b046d113696a0406d081b55ec74e28 # feat/epoch-4-rc - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za From f209226c594a64bb71da04c0267671eaa9cb0fd9 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:07:24 -0700 Subject: [PATCH 15/30] fix: update signer-manager, sbtc_registry, bond admin --- stacking/Clarinet.toml | 8 +- stacking/clarigen-types.ts | 217 +++-------- ...KAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar | 2 +- stacking/contracts/pox-5-signer.clar | 55 ++- stacking/contracts/pox-5.clar | 342 ++++++++---------- stacking/deployments/default.simnet-plan.yaml | 6 +- stacking/package.json | 5 +- stacks-krypton-miner.toml | 2 + 8 files changed, 244 insertions(+), 393 deletions(-) diff --git a/stacking/Clarinet.toml b/stacking/Clarinet.toml index 5001815..831ceff 100644 --- a/stacking/Clarinet.toml +++ b/stacking/Clarinet.toml @@ -10,13 +10,13 @@ costs_version = 1 [contracts.pox-5] path = "./contracts/pox-5.clar" -clarity_version = 4 -epoch = 3.3 +clarity_version = 6 +epoch = 4.0 [contracts.pox-5-signer] path = "./contracts/pox-5-signer.clar" -clarity_version = 4 -epoch = 3.3 +clarity_version = 6 +epoch = 4.0 [repl.analysis.lints] unused_const = "warn" diff --git a/stacking/clarigen-types.ts b/stacking/clarigen-types.ts index 5b51fb5..807410f 100644 --- a/stacking/clarigen-types.ts +++ b/stacking/clarigen-types.ts @@ -54,15 +54,6 @@ export const contracts = { "calculationHeight": bigint; "lastBondIndex": bigint | null; "lastBondStxValueRatio": bigint | null; -}, bigint>>, - crystallizeRewards: {"name":"crystallize-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], { - "earned": bigint; - "rewardsPerToken": bigint; -}>, - getBitcoinTxOutput_q: {"name":"get-bitcoin-tx-output?","access":"private","args":[{"name":"tx-bytes","type":{"buffer":{"length":100000}}},{"name":"output-index","type":"uint128"},{"name":"amount","type":"uint128"},{"name":"script","type":{"buffer":{"length":34}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"amount","type":"uint128"},{"name":"script","type":{"buffer":{"length":34}}},{"name":"txid","type":{"buffer":{"length":32}}}]},"error":"uint128"}}}} as TypedAbiFunction<[txBytes: TypedAbiArg, outputIndex: TypedAbiArg, amount: TypedAbiArg, script: TypedAbiArg], Response<{ - "amount": bigint; - "script": Uint8Array; - "txid": Uint8Array; }, bigint>>, lockSbtc: {"name":"lock-sbtc","access":"private","args":[{"name":"amount","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[amount: TypedAbiArg], Response>, matchUintInList: {"name":"match-uint-in-list","access":"private","args":[{"name":"item","type":"uint128"},{"name":"acc","type":{"tuple":[{"name":"found","type":"bool"},{"name":"needle","type":"uint128"}]}}],"outputs":{"type":{"tuple":[{"name":"found","type":"bool"},{"name":"needle","type":"uint128"}]}}} as TypedAbiFunction<[item: TypedAbiArg, acc: TypedAbiArg<{ @@ -88,6 +79,10 @@ export const contracts = { "staker": string; }, bigint>>, reverseBuff16: {"name":"reverse-buff16","access":"private","args":[{"name":"input","type":{"buffer":{"length":16}}}],"outputs":{"type":{"buffer":{"length":17}}}} as TypedAbiFunction<[input: TypedAbiArg], Uint8Array>, + settleRewards: {"name":"settle-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], { + "earned": bigint; + "rewardsPerToken": bigint; +}>, updateClaimableBondRewards: {"name":"update-claimable-bond-rewards","access":"private","args":[{"name":"bond-index","type":"uint128"},{"name":"accumulator","type":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"signer","type":"principal"},{"name":"total","type":"uint128"}]}}],"outputs":{"type":{"tuple":[{"name":"bond-rewards","type":{"list":{"type":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]},"length":6}}},{"name":"signer","type":"principal"},{"name":"total","type":"uint128"}]}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, accumulator: TypedAbiArg<{ "bondRewards": { "bondIndex": number | bigint; @@ -105,7 +100,7 @@ export const contracts = { "signer": string; "total": bigint; }>, - updateClaimableRewards: {"name":"update-claimable-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], { + updateClaimableRewards: {"name":"update-claimable-rewards","access":"private","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], { "earned": bigint; "rewardsPerToken": bigint; }>, @@ -138,7 +133,6 @@ export const contracts = { }[]; "unlockBytes": Uint8Array; }, "lockups">], Response>, - verifyMerkleProof: {"name":"verify-merkle-proof","access":"private","args":[{"name":"leaf-hash","type":{"buffer":{"length":32}}},{"name":"root-hash","type":{"buffer":{"length":32}}},{"name":"tx-index","type":"uint128"},{"name":"tx-count","type":"uint128"},{"name":"leaf-hashes","type":{"list":{"type":{"buffer":{"length":32}},"length":14}}}],"outputs":{"type":"bool"}} as TypedAbiFunction<[leafHash: TypedAbiArg, rootHash: TypedAbiArg, txIndex: TypedAbiArg, txCount: TypedAbiArg, leafHashes: TypedAbiArg], boolean>, allowContractCaller: {"name":"allow-contract-caller","access":"public","args":[{"name":"caller","type":"principal"},{"name":"until-burn-ht","type":{"optional":"uint128"}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[caller: TypedAbiArg, untilBurnHt: TypedAbiArg], Response>, announceL1EarlyExit: {"name":"announce-l1-early-exit","access":"public","args":[{"name":"staker","type":"principal"},{"name":"old-signer-manager","type":"trait_reference"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[staker: TypedAbiArg, oldSignerManager: TypedAbiArg], Response>, calculateRewards: {"name":"calculate-rewards","access":"public","args":[{"name":"bond-periods","type":{"list":{"type":"uint128","length":6}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[bondPeriods: TypedAbiArg], Response>, @@ -185,12 +179,12 @@ export const contracts = { revokeSignerGrant: {"name":"revoke-signer-grant","access":"public","args":[{"name":"signer-manager","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg], Response>, setBondAdmin: {"name":"set-bond-admin","access":"public","args":[{"name":"new-admin","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[newAdmin: TypedAbiArg], Response>, setBurnchainParameters: {"name":"set-burnchain-parameters","access":"public","args":[{"name":"first-burn-height","type":"uint128"},{"name":"prepare-cycle-length","type":"uint128"},{"name":"reward-cycle-length","type":"uint128"},{"name":"begin-pox5-reward-cycle","type":"uint128"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[firstBurnHeight: TypedAbiArg, prepareCycleLength: TypedAbiArg, rewardCycleLength: TypedAbiArg, beginPox5RewardCycle: TypedAbiArg], Response>, - setupBond: {"name":"setup-bond","access":"public","args":[{"name":"bond-index","type":"uint128"},{"name":"target-rate","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"early-unlock-admin","type":"principal"},{"name":"allowlist","type":{"list":{"type":{"tuple":[{"name":"max-sats","type":"uint128"},{"name":"staker","type":"principal"}]},"length":1000}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"max-allocation-sats","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, targetRate: TypedAbiArg, stxValueRatio: TypedAbiArg, minUstxRatio: TypedAbiArg, earlyUnlockSigners: TypedAbiArg, earlyUnlockAdmin: TypedAbiArg, allowlist: TypedAbiArg<{ + setupBond: {"name":"setup-bond","access":"public","args":[{"name":"bond-index","type":"uint128"},{"name":"target-rate","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-admin","type":"principal"},{"name":"allowlist","type":{"list":{"type":{"tuple":[{"name":"max-sats","type":"uint128"},{"name":"staker","type":"principal"}]},"length":1000}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"bond-index","type":"uint128"},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}},{"name":"max-allocation-sats","type":"uint128"},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]},"error":"uint128"}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg, targetRate: TypedAbiArg, stxValueRatio: TypedAbiArg, minUstxRatio: TypedAbiArg, earlyUnlockBytes: TypedAbiArg, earlyUnlockAdmin: TypedAbiArg, allowlist: TypedAbiArg<{ "maxSats": number | bigint; "staker": string; }[], "allowlist">], Response<{ "bondIndex": bigint; - "earlyUnlockSigners": Uint8Array; + "earlyUnlockBytes": Uint8Array; "maxAllocationSats": bigint; "minUstxRatio": bigint; "stxValueRatio": bigint; @@ -235,7 +229,7 @@ export const contracts = { checkCallerAllowed: {"name":"check-caller-allowed","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"bool","error":"uint128"}}}} as TypedAbiFunction<[], Response>, checkPoxLockPeriod: {"name":"check-pox-lock-period","access":"read_only","args":[{"name":"lock-period","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[lockPeriod: TypedAbiArg], boolean>, constructLockupOutputScript: {"name":"construct-lockup-output-script","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"buffer":{"length":34}}}} as TypedAbiFunction<[staker: TypedAbiArg, unlockBurnHeight: TypedAbiArg, unlockBytes: TypedAbiArg, earlyUnlockBytes: TypedAbiArg], Uint8Array>, - constructLockupScript: {"name":"construct-lockup-script","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"buffer":{"length":5141}}}} as TypedAbiFunction<[staker: TypedAbiArg, unlockBurnHeight: TypedAbiArg, unlockBytes: TypedAbiArg, earlyUnlockBytes: TypedAbiArg], Uint8Array>, + constructLockupScript: {"name":"construct-lockup-script","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"unlock-burn-height","type":"uint128"},{"name":"unlock-bytes","type":{"buffer":{"length":683}}},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}}],"outputs":{"type":{"buffer":{"length":4109}}}} as TypedAbiFunction<[staker: TypedAbiArg, unlockBurnHeight: TypedAbiArg, unlockBytes: TypedAbiArg, earlyUnlockBytes: TypedAbiArg], Uint8Array>, currentDistributionCycle: {"name":"current-distribution-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, currentPoxRewardCycle: {"name":"current-pox-reward-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, distributionCycleToBurnHeight: {"name":"distribution-cycle-to-burn-height","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[cycle: TypedAbiArg], bigint>, @@ -249,7 +243,8 @@ export const contracts = { "isL1Lock": boolean; "signer": string; } | null>, - getEarned: {"name":"get-earned","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getEarned: {"name":"get-earned","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getFirstPox5RewardCycle: {"name":"get-first-pox-5-reward-cycle","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, getLastAccountedRewardsOnly: {"name":"get-last-accounted-rewards-only","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, getLastRewardComputeHeight: {"name":"get-last-reward-compute-height","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, getNewRewards: {"name":"get-new-rewards","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, @@ -261,9 +256,9 @@ export const contracts = { "rewardCycleLength": bigint; "totalLiquidSupplyUstx": bigint; }, null>>, - getProtocolBond: {"name":"get-protocol-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"early-unlock-admin","type":"principal"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg], { + getProtocolBond: {"name":"get-protocol-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"early-unlock-admin","type":"principal"},{"name":"early-unlock-bytes","type":{"buffer":{"length":683}}},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]}}}} as TypedAbiFunction<[bondIndex: TypedAbiArg], { "earlyUnlockAdmin": string; - "earlyUnlockSigners": Uint8Array; + "earlyUnlockBytes": Uint8Array; "minUstxRatio": bigint; "stxValueRatio": bigint; "targetRate": bigint; @@ -271,16 +266,15 @@ export const contracts = { getReserveBalance: {"name":"get-reserve-balance","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, getReversedTxid: {"name":"get-reversed-txid","access":"read_only","args":[{"name":"tx","type":{"buffer":{"length":100000}}}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[tx: TypedAbiArg], Uint8Array>, getRewards: {"name":"get-rewards","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, - getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], bigint>, getSignerCycleMembership: {"name":"get-signer-cycle-membership","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg, cycle: TypedAbiArg], { "amountUstx": bigint; "signer": string; } | null>, getSignerGrantMessageHash: {"name":"get-signer-grant-message-hash","access":"read_only","args":[{"name":"signer-manager","type":"principal"},{"name":"auth-id","type":"uint128"}],"outputs":{"type":{"buffer":{"length":32}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, authId: TypedAbiArg], Uint8Array>, getSignerInfo: {"name":"get-signer-info","access":"read_only","args":[{"name":"signer","type":"principal"}],"outputs":{"type":{"optional":{"buffer":{"length":33}}}}} as TypedAbiFunction<[signer: TypedAbiArg], Uint8Array | null>, - getSignerPendingRewardsForCycle: {"name":"get-signer-pending-rewards-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, getSignerPendingStakedUstxPerCycle: {"name":"get-signer-pending-staked-ustx-per-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], bigint>, - getSignerRewardsPerTokenPaidForCycle: {"name":"get-signer-rewards-per-token-paid-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getSignerRewardsPerTokenSettledForCycle: {"name":"get-signer-rewards-per-token-settled-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, getSignerSetFirstItemForCycle: {"name":"get-signer-set-first-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, getSignerSetItemForCycle: {"name":"get-signer-set-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":{"tuple":[{"name":"next","type":{"optional":"principal"}},{"name":"prev","type":{"optional":"principal"}}]}}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], { "next": string | null; @@ -289,17 +283,18 @@ export const contracts = { getSignerSetLastItemForCycle: {"name":"get-signer-set-last-item-for-cycle","access":"read_only","args":[{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[cycle: TypedAbiArg], string | null>, getSignerSetNextItemForCycle: {"name":"get-signer-set-next-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], string | null>, getSignerSetPrevItemForCycle: {"name":"get-signer-set-prev-item-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"cycle","type":"uint128"}],"outputs":{"type":{"optional":"principal"}}} as TypedAbiFunction<[signer: TypedAbiArg, cycle: TypedAbiArg], string | null>, - getSignerSharesStakedForCycle: {"name":"get-signer-shares-staked-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getSignerSharesStakedForCycle: {"name":"get-signer-shares-staked-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getSignerUnclaimedRewardsForCycle: {"name":"get-signer-unclaimed-rewards-for-cycle","access":"read_only","args":[{"name":"signer","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[signer: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, getStakerInfo: {"name":"get-staker-info","access":"read_only","args":[{"name":"staker","type":"principal"}],"outputs":{"type":{"optional":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"signer","type":"principal"}]}}}} as TypedAbiFunction<[staker: TypedAbiArg], { "amountUstx": bigint; "firstRewardCycle": bigint; "numCycles": bigint; "signer": string; } | null>, - getStakerSharesStakedForCycle: {"name":"get-staker-shares-staked-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg, signer: TypedAbiArg], bigint>, + getStakerSharesStakedForCycle: {"name":"get-staker-shares-staked-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"},{"name":"signer","type":"principal"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg, signer: TypedAbiArg], bigint>, getTotalSbtcStaked: {"name":"get-total-sbtc-staked","access":"read_only","args":[],"outputs":{"type":"uint128"}} as TypedAbiFunction<[], bigint>, getTotalSbtcStakedForBond: {"name":"get-total-sbtc-staked-for-bond","access":"read_only","args":[{"name":"bond-index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[bondIndex: TypedAbiArg], bigint>, - getTotalSharesStakedForCycle: {"name":"get-total-shares-staked-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, + getTotalSharesStakedForCycle: {"name":"get-total-shares-staked-for-cycle","access":"read_only","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], bigint>, getTotalUstxStacked: {"name":"get-total-ustx-stacked","access":"read_only","args":[{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[rewardCycle: TypedAbiArg], bigint>, getUstxDelegatedForCycle: {"name":"get-ustx-delegated-for-cycle","access":"read_only","args":[{"name":"reward-cycle","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[rewardCycle: TypedAbiArg], bigint>, isBondActiveAtHeight: {"name":"is-bond-active-at-height","access":"read_only","args":[{"name":"bond-index","type":"uint128"},{"name":"calculation-height","type":"uint128"}],"outputs":{"type":"bool"}} as TypedAbiFunction<[bondIndex: TypedAbiArg, calculationHeight: TypedAbiArg], boolean>, @@ -359,9 +354,9 @@ export const contracts = { "isL1Lock": boolean; "signer": string; }>, - protocolBonds: {"name":"protocol-bonds","key":"uint128","value":{"tuple":[{"name":"early-unlock-admin","type":"principal"},{"name":"early-unlock-signers","type":{"buffer":{"length":683}}},{"name":"min-ustx-ratio","type":"uint128"},{"name":"stx-value-ratio","type":"uint128"},{"name":"target-rate","type":"uint128"}]}} as TypedAbiMap, - signerPendingRewardsForCycle: {"name":"signer-pending-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ - "index": number | bigint; - "isBond": boolean; - "signer": string; -}, bigint>, signerPendingStakedUstxPerCycle: {"name":"signer-pending-staked-ustx-per-cycle","key":{"tuple":[{"name":"cycle","type":"uint128"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ "cycle": number | bigint; "signer": string; }, bigint>, - signerRewardsPerTokenPaidForCycle: {"name":"signer-rewards-per-token-paid-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + signerRewardsPerTokenSettledForCycle: {"name":"signer-rewards-per-token-settled-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ "index": number | bigint; "isBond": boolean; "signer": string; @@ -406,6 +396,11 @@ export const contracts = { "index": number | bigint; "isBond": boolean; "signer": string; +}, bigint>, + signerUnclaimedRewardsForCycle: {"name":"signer-unclaimed-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + "index": number | bigint; + "isBond": boolean; + "signer": string; }, bigint>, signers: {"name":"signers","key":"principal","value":{"buffer":{"length":33}}} as TypedAbiMap, stakerInfo: {"name":"staker-info","key":"principal","value":{"tuple":[{"name":"amount-ustx","type":"uint128"},{"name":"first-reward-cycle","type":"uint128"},{"name":"num-cycles","type":"uint128"},{"name":"signer","type":"principal"}]}} as TypedAbiMap, - usedSignerKeyAuthorizations: {"name":"used-signer-key-authorizations","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"max-amount","type":"uint128"},{"name":"period","type":"uint128"},{"name":"pox-addr","type":{"optional":{"tuple":[{"name":"hashbytes","type":{"buffer":{"length":32}}},{"name":"version","type":{"buffer":{"length":1}}}]}}},{"name":"reward-cycle","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"topic","type":{"string-ascii":{"length":14}}}]},"value":"bool"} as TypedAbiMap<{ - "authId": number | bigint; - "maxAmount": number | bigint; - "period": number | bigint; - "poxAddr": { - "hashbytes": Uint8Array; - "version": Uint8Array; -} | null; - "rewardCycle": number | bigint; - "signerKey": Uint8Array; - "topic": string; -}, boolean>, usedSignerKeyGrants: {"name":"used-signer-key-grants","key":{"tuple":[{"name":"auth-id","type":"uint128"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"signer-manager","type":"principal"}]},"value":"bool"} as TypedAbiMap<{ "authId": number | bigint; "signerKey": Uint8Array; @@ -611,8 +594,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_LOCKUP_SCRIPT: { - name: 'ERR_INVALID_LOCKUP_SCRIPT', + ERR_INVALID_LOCKUP_AMOUNT: { + name: 'ERR_INVALID_LOCKUP_AMOUNT', type: { response: { ok: 'none', @@ -621,8 +604,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_MERKLE_PROOF: { - name: 'ERR_INVALID_MERKLE_PROOF', + ERR_INVALID_LOCKUP_SCRIPT: { + name: 'ERR_INVALID_LOCKUP_SCRIPT', type: { response: { ok: 'none', @@ -631,8 +614,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_NUM_CYCLES: { - name: 'ERR_INVALID_NUM_CYCLES', + ERR_INVALID_MERKLE_PROOF: { + name: 'ERR_INVALID_MERKLE_PROOF', type: { response: { ok: 'none', @@ -641,8 +624,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_OLD_SIGNER_MANAGER: { - name: 'ERR_INVALID_OLD_SIGNER_MANAGER', + ERR_INVALID_NUM_CYCLES: { + name: 'ERR_INVALID_NUM_CYCLES', type: { response: { ok: 'none', @@ -651,8 +634,8 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - ERR_INVALID_POX_ADDRESS: { - name: 'ERR_INVALID_POX_ADDRESS', + ERR_INVALID_OLD_SIGNER_MANAGER: { + name: 'ERR_INVALID_OLD_SIGNER_MANAGER', type: { response: { ok: 'none', @@ -700,16 +683,6 @@ export const contracts = { } }, access: 'constant' -} as TypedAbiVariable>, - eRR_L1_LOCKUP_NOT_FOUND: { - name: 'ERR_L1_LOCKUP_NOT_FOUND', - type: { - response: { - ok: 'none', - error: 'uint128' - } - }, - access: 'constant' } as TypedAbiVariable>, ERR_NOT_ALLOWLISTED: { name: 'ERR_NOT_ALLOWLISTED', @@ -750,16 +723,6 @@ export const contracts = { } }, access: 'constant' -} as TypedAbiVariable>, - ERR_NO_SBTC_BALANCE: { - name: 'ERR_NO_SBTC_BALANCE', - type: { - response: { - ok: 'none', - error: 'uint128' - } - }, - access: 'constant' } as TypedAbiVariable>, ERR_READ_TX_OUT_OF_BOUNDS: { name: 'ERR_READ_TX_OUT_OF_BOUNDS', @@ -770,26 +733,6 @@ export const contracts = { } }, access: 'constant' -} as TypedAbiVariable>, - ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { - name: 'ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH', - type: { - response: { - ok: 'none', - error: 'uint128' - } - }, - access: 'constant' -} as TypedAbiVariable>, - ERR_SIGNER_AUTH_USED: { - name: 'ERR_SIGNER_AUTH_USED', - type: { - response: { - ok: 'none', - error: 'uint128' - } - }, - access: 'constant' } as TypedAbiVariable>, ERR_SIGNER_KEY_GRANT_NOT_FOUND: { name: 'ERR_SIGNER_KEY_GRANT_NOT_FOUND', @@ -800,16 +743,6 @@ export const contracts = { } }, access: 'constant' -} as TypedAbiVariable>, - ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { - name: 'ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH', - type: { - response: { - ok: 'none', - error: 'uint128' - } - }, - access: 'constant' } as TypedAbiVariable>, ERR_SIGNER_KEY_GRANT_USED: { name: 'ERR_SIGNER_KEY_GRANT_USED', @@ -901,21 +834,6 @@ export const contracts = { }, access: 'constant' } as TypedAbiVariable>, - MAX_ADDRESS_VERSION: { - name: 'MAX_ADDRESS_VERSION', - type: 'uint128', - access: 'constant' -} as TypedAbiVariable, - mAX_ADDRESS_VERSION_BUFF_20: { - name: 'MAX_ADDRESS_VERSION_BUFF_20', - type: 'uint128', - access: 'constant' -} as TypedAbiVariable, - mAX_ADDRESS_VERSION_BUFF_32: { - name: 'MAX_ADDRESS_VERSION_BUFF_32', - type: 'uint128', - access: 'constant' -} as TypedAbiVariable, MAX_NUM_CYCLES: { name: 'MAX_NUM_CYCLES', type: 'uint128', @@ -1114,6 +1032,10 @@ export const contracts = { isOk: false, value: 40n }, + ERR_INVALID_LOCKUP_AMOUNT: { + isOk: false, + value: 45n + }, ERR_INVALID_LOCKUP_SCRIPT: { isOk: false, value: 42n @@ -1130,10 +1052,6 @@ export const contracts = { isOk: false, value: 36n }, - ERR_INVALID_POX_ADDRESS: { - isOk: false, - value: 21n - }, ERR_INVALID_SIGNATURE_PUBKEY: { isOk: false, value: 14n @@ -1150,10 +1068,6 @@ export const contracts = { isOk: false, value: 37n }, - eRR_L1_LOCKUP_NOT_FOUND: { - isOk: false, - value: 6n - }, ERR_NOT_ALLOWLISTED: { isOk: false, value: 11n @@ -1170,30 +1084,14 @@ export const contracts = { isOk: false, value: 32n }, - ERR_NO_SBTC_BALANCE: { - isOk: false, - value: 25n - }, ERR_READ_TX_OUT_OF_BOUNDS: { isOk: false, value: 39n }, - ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH: { - isOk: false, - value: 15n - }, - ERR_SIGNER_AUTH_USED: { - isOk: false, - value: 16n - }, ERR_SIGNER_KEY_GRANT_NOT_FOUND: { isOk: false, value: 17n }, - ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH: { - isOk: false, - value: 18n - }, ERR_SIGNER_KEY_GRANT_USED: { isOk: false, value: 12n @@ -1230,9 +1128,6 @@ export const contracts = { isOk: false, value: 44n }, - MAX_ADDRESS_VERSION: 6n, - mAX_ADDRESS_VERSION_BUFF_20: 4n, - mAX_ADDRESS_VERSION_BUFF_32: 6n, MAX_NUM_CYCLES: 96n, pOX_5_SIGNER_DOMAIN: { chainId: 2_147_483_648n, @@ -1260,7 +1155,7 @@ export const contracts = { "non_fungible_tokens": [ ], - "fungible_tokens":[],"epoch":"Epoch33","clarity_version":"Clarity4", + "fungible_tokens":[],"epoch":"Epoch40","clarity_version":"Clarity6", contractName: 'pox-5', }, pox5Signer: { @@ -1274,7 +1169,7 @@ pox5Signer: { "isBond": boolean; "staker": string; }, bigint>>, - crystallizeStakerRewards: {"name":"crystallize-staker-rewards","access":"private","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], { + settleStakerRewards: {"name":"settle-staker-rewards","access":"private","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"tuple":[{"name":"earned","type":"uint128"},{"name":"rewards-per-token","type":"uint128"}]}}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], { "earned": bigint; "rewardsPerToken": bigint; }>, @@ -1298,29 +1193,28 @@ pox5Signer: { }; "totalRewards": bigint; }, bigint>>, - claimStakerRewards: {"name":"claim-staker-rewards","access":"public","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], Response>, + claimStakerRewards: {"name":"claim-staker-rewards","access":"public","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":{"response":{"ok":"uint128","error":"uint128"}}}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], Response>, registerSelf: {"name":"register-self","access":"public","args":[{"name":"signer-manager","type":"trait_reference"},{"name":"signer-key","type":{"buffer":{"length":33}}},{"name":"auth-id","type":"uint128"},{"name":"signer-sig","type":{"buffer":{"length":65}}}],"outputs":{"type":{"response":{"ok":{"tuple":[{"name":"signer","type":"principal"},{"name":"signer-key","type":{"buffer":{"length":33}}}]},"error":"uint128"}}}} as TypedAbiFunction<[signerManager: TypedAbiArg, signerKey: TypedAbiArg, authId: TypedAbiArg, signerSig: TypedAbiArg], Response<{ "signer": string; "signerKey": Uint8Array; }, bigint>>, - updateAllowedCaller: {"name":"update-allowed-caller","access":"public","args":[{"name":"new-allowed-caller","type":"principal"}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[newAllowedCaller: TypedAbiArg], Response>, validateStake_x: {"name":"validate-stake!","access":"public","args":[{"name":"staker","type":"principal"},{"name":"first-index","type":"uint128"},{"name":"num-indexes","type":"uint128"},{"name":"amount-ustx","type":"uint128"},{"name":"amount-sats","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"signer-calldata","type":{"optional":{"buffer":{"length":500}}}}],"outputs":{"type":{"response":{"ok":"bool","error":"none"}}}} as TypedAbiFunction<[staker: TypedAbiArg, firstIndex: TypedAbiArg, numIndexes: TypedAbiArg, amountUstx: TypedAbiArg, amountSats: TypedAbiArg, isBond: TypedAbiArg, signerCalldata: TypedAbiArg], Response>, - getEarnedStakerRewards: {"name":"get-earned-staker-rewards","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, - getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[index: TypedAbiArg, isBond: TypedAbiArg], bigint>, - getStakerPendingRewardsForCycle: {"name":"get-staker-pending-rewards-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint>, - getStakerRewardsPerTokenPaidForCycle: {"name":"get-staker-rewards-per-token-paid-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, index: TypedAbiArg, isBond: TypedAbiArg], bigint> + getEarnedStakerRewards: {"name":"get-earned-staker-rewards","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getRewardsPerTokenForCycle: {"name":"get-rewards-per-token-for-cycle","access":"read_only","args":[{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getStakerRewardsPerTokenSettledForCycle: {"name":"get-staker-rewards-per-token-settled-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint>, + getStakerUnclaimedRewardsForCycle: {"name":"get-staker-unclaimed-rewards-for-cycle","access":"read_only","args":[{"name":"staker","type":"principal"},{"name":"is-bond","type":"bool"},{"name":"index","type":"uint128"}],"outputs":{"type":"uint128"}} as TypedAbiFunction<[staker: TypedAbiArg, isBond: TypedAbiArg, index: TypedAbiArg], bigint> }, "maps": { rewardsPerTokenForCycle: {"name":"rewards-per-token-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"}]},"value":"uint128"} as TypedAbiMap<{ "index": number | bigint; "isBond": boolean; }, bigint>, - stakerPendingRewardsForCycle: {"name":"staker-pending-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + stakerRewardsPerTokenSettledForCycle: {"name":"staker-rewards-per-token-settled-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ "index": number | bigint; "isBond": boolean; "staker": string; }, bigint>, - stakerRewardsPaidPerTokenForCycle: {"name":"staker-rewards-paid-per-token-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ + stakerUnclaimedRewardsForCycle: {"name":"staker-unclaimed-rewards-for-cycle","key":{"tuple":[{"name":"index","type":"uint128"},{"name":"is-bond","type":"bool"},{"name":"staker","type":"principal"}]},"value":"uint128"} as TypedAbiMap<{ "index": number | bigint; "isBond": boolean; "staker": string; @@ -1341,25 +1235,19 @@ pox5Signer: { name: 'PRECISION', type: 'uint128', access: 'constant' -} as TypedAbiVariable, - allowedCaller: { - name: 'allowed-caller', - type: 'principal', - access: 'variable' -} as TypedAbiVariable +} as TypedAbiVariable }, constants: { ERR_NO_CLAIMABLE_REWARDS: { isOk: false, value: 1_001n }, - PRECISION: 1_000_000_000_000_000_000n, - allowedCaller: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM' + PRECISION: 1_000_000_000_000_000_000n }, "non_fungible_tokens": [ ], - "fungible_tokens":[],"epoch":"Epoch33","clarity_version":"Clarity4", + "fungible_tokens":[],"epoch":"Epoch40","clarity_version":"Clarity6", contractName: 'pox-5-signer', }, sbtcRegistry: { @@ -1583,6 +1471,7 @@ sbtcToken: { getBalance: {"name":"get-balance","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, getBalanceAvailable: {"name":"get-balance-available","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, getBalanceLocked: {"name":"get-balance-locked","access":"read_only","args":[{"name":"who","type":"principal"}],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[who: TypedAbiArg], Response>, + getCurrentAggregatePubkey: {"name":"get-current-aggregate-pubkey","access":"read_only","args":[],"outputs":{"type":{"buffer":{"length":33}}}} as TypedAbiFunction<[], Uint8Array>, getDecimals: {"name":"get-decimals","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":"uint128","error":"none"}}}} as TypedAbiFunction<[], Response>, getName: {"name":"get-name","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"string-ascii":{"length":32}},"error":"none"}}}} as TypedAbiFunction<[], Response>, getSymbol: {"name":"get-symbol","access":"read_only","args":[],"outputs":{"type":{"response":{"ok":{"string-ascii":{"length":10}},"error":"none"}}}} as TypedAbiFunction<[], Response>, diff --git a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar index 8d4281d..b3f5672 100644 --- a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar +++ b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar @@ -14,7 +14,7 @@ (define-data-var last-withdrawal-request-id uint u0) (define-data-var current-signature-threshold uint u0) (define-data-var current-signer-set (list 128 (buff 33)) (list)) -(define-data-var current-aggregate-pubkey (buff 33) 0x00) +(define-data-var current-aggregate-pubkey (buff 33) 0x02158613a973bb4469dc9713e0a330a30b6cb88580b772658990a0b052149ca42a) (define-data-var current-signer-principal principal tx-sender) ;; Maps diff --git a/stacking/contracts/pox-5-signer.clar b/stacking/contracts/pox-5-signer.clar index 690e73a..4e11a18 100644 --- a/stacking/contracts/pox-5-signer.clar +++ b/stacking/contracts/pox-5-signer.clar @@ -7,9 +7,6 @@ ;; during reward calculations (define-constant PRECISION u1000000000000000000) ;; 1e18 -;; default to allowing deployer to register as a pool -(define-data-var allowed-caller principal tx-sender) - (define-map rewards-per-token-for-cycle { index: uint, @@ -18,7 +15,7 @@ uint ) -(define-map staker-rewards-paid-per-token-for-cycle +(define-map staker-rewards-per-token-settled-for-cycle { is-bond: bool, index: uint, @@ -28,7 +25,7 @@ ) ;; Represents pending, but unclaimed rewards for a staker -(define-map staker-pending-rewards-for-cycle +(define-map staker-unclaimed-rewards-for-cycle { is-bond: bool, index: uint, @@ -57,10 +54,6 @@ (ok true) ) -(define-public (update-allowed-caller (new-allowed-caller principal)) - (ok (var-set allowed-caller new-allowed-caller)) -) - (define-public (register-self (signer-manager ) (signer-key (buff 33)) @@ -121,28 +114,28 @@ (staker (get staker acc)) (index (+ (get first-index acc) index-offset)) ) - (crystallize-staker-rewards staker index (get is-bond acc)) + (settle-staker-rewards staker (get is-bond acc) index) (ok acc) ) ) -(define-private (crystallize-staker-rewards +(define-private (settle-staker-rewards (staker principal) - (index uint) (is-bond bool) + (index uint) ) (let ( - (earned (get-earned-staker-rewards staker index is-bond)) - (rewards-per-token (get-rewards-per-token-for-cycle index is-bond)) + (earned (get-earned-staker-rewards staker is-bond index)) + (rewards-per-token (get-rewards-per-token-for-cycle is-bond index)) ) - (map-set staker-pending-rewards-for-cycle { + (map-set staker-unclaimed-rewards-for-cycle { staker: staker, index: index, is-bond: is-bond, } earned ) - (map-set staker-rewards-paid-per-token-for-cycle { + (map-set staker-rewards-per-token-settled-for-cycle { staker: staker, index: index, is-bond: is-bond, @@ -178,16 +171,16 @@ ;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` (define-read-only (get-earned-staker-rewards (staker principal) - (index uint) (is-bond bool) + (index uint) ) (let ( (shares (contract-call? .pox-5 get-staker-shares-staked-for-cycle staker - index is-bond current-contract + is-bond index current-contract )) - (rpt-current (get-rewards-per-token-for-cycle index is-bond)) - (rpt-paid (get-staker-rewards-per-token-paid-for-cycle staker index is-bond)) - (pending (get-staker-pending-rewards-for-cycle staker index is-bond)) + (rpt-current (get-rewards-per-token-for-cycle is-bond index)) + (rpt-paid (get-staker-rewards-per-token-settled-for-cycle staker is-bond index)) + (pending (get-staker-unclaimed-rewards-for-cycle staker is-bond index)) (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) ) (+ pending newly-earned) @@ -195,16 +188,16 @@ ) (define-public (claim-staker-rewards - (index uint) (is-bond bool) + (index uint) ) (let ( (staker tx-sender) - (rewards-info (crystallize-staker-rewards staker index is-bond)) + (rewards-info (settle-staker-rewards staker is-bond index)) (earned (get earned rewards-info)) ) (asserts! (> earned u0) ERR_NO_CLAIMABLE_REWARDS) - (map-set staker-pending-rewards-for-cycle { + (map-set staker-unclaimed-rewards-for-cycle { staker: staker, is-bond: is-bond, index: index, @@ -256,8 +249,8 @@ ) (define-read-only (get-rewards-per-token-for-cycle - (index uint) (is-bond bool) + (index uint) ) (default-to u0 (map-get? rewards-per-token-for-cycle { @@ -267,13 +260,13 @@ ) ) -(define-read-only (get-staker-rewards-per-token-paid-for-cycle +(define-read-only (get-staker-rewards-per-token-settled-for-cycle (staker principal) - (index uint) (is-bond bool) + (index uint) ) (default-to u0 - (map-get? staker-rewards-paid-per-token-for-cycle { + (map-get? staker-rewards-per-token-settled-for-cycle { staker: staker, index: index, is-bond: is-bond, @@ -281,13 +274,13 @@ ) ) -(define-read-only (get-staker-pending-rewards-for-cycle +(define-read-only (get-staker-unclaimed-rewards-for-cycle (staker principal) - (index uint) (is-bond bool) + (index uint) ) (default-to u0 - (map-get? staker-pending-rewards-for-cycle { + (map-get? staker-unclaimed-rewards-for-cycle { staker: staker, index: index, is-bond: is-bond, diff --git a/stacking/contracts/pox-5.clar b/stacking/contracts/pox-5.clar index fb35809..137cfc5 100644 --- a/stacking/contracts/pox-5.clar +++ b/stacking/contracts/pox-5.clar @@ -3,7 +3,6 @@ (define-constant ERR_CANNOT_SETUP_BOND_TOO_LATE (err u3)) (define-constant ERR_BOND_ALREADY_SETUP (err u4)) (define-constant ERR_STAKER_ALREADY_ADDED (err u5)) -(define-constant ERR_L1_LOCKUP_NOT_FOUND (err u6)) (define-constant ERR_BOND_NOT_FOUND (err u7)) (define-constant ERR_INSUFFICIENT_STX (err u8)) (define-constant ERR_ALREADY_REGISTERED (err u9)) @@ -12,17 +11,12 @@ (define-constant ERR_SIGNER_KEY_GRANT_USED (err u12)) (define-constant ERR_INVALID_SIGNATURE_RECOVER (err u13)) (define-constant ERR_INVALID_SIGNATURE_PUBKEY (err u14)) -(define-constant ERR_SIGNER_AUTH_AMOUNT_TOO_HIGH (err u15)) -(define-constant ERR_SIGNER_AUTH_USED (err u16)) (define-constant ERR_SIGNER_KEY_GRANT_NOT_FOUND (err u17)) -(define-constant ERR_SIGNER_KEY_GRANT_POX_ADDR_MISMATCH (err u18)) (define-constant ERR_ALREADY_STAKED (err u19)) (define-constant ERR_INVALID_NUM_CYCLES (err u20)) -(define-constant ERR_INVALID_POX_ADDRESS (err u21)) (define-constant ERR_UNAUTHORIZED_CALLER (err u22)) (define-constant ERR_SIGNER_NOT_FOUND (err u23)) (define-constant ERR_INVALID_START_BURN_HEIGHT (err u24)) -(define-constant ERR_NO_SBTC_BALANCE (err u25)) (define-constant ERR_UNAUTHORIZED_SIGNER_REGISTRATION (err u26)) (define-constant ERR_NOT_STAKING (err u27)) (define-constant ERR_UNSTAKE_IN_PREPARE_PHASE (err u28)) @@ -56,6 +50,8 @@ (define-constant ERR_BOND_ALREADY_STARTED (err u43)) ;; Cannot call `update-bond-registration` with the same signer (define-constant ERR_UPDATE_BOND_SAME_SIGNER (err u44)) +;; The lockup amount does not match the specified amount of sats +(define-constant ERR_INVALID_LOCKUP_AMOUNT (err u45)) ;; The length, in terms of staking cycles, of a given ;; bond period @@ -79,16 +75,6 @@ chain-id: chain-id, }) -;; Keep these constants in lock-step with the address version buffs above -;; Maximum value of an address version as a uint -(define-constant MAX_ADDRESS_VERSION u6) -;; Maximum value of an address version that has a 20-byte hashbytes -;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes) -(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4) -;; Maximum value of an address version that has a 32-byte hashbytes -;; (0x05 and 0x06 have 32-byte hashbytes) -(define-constant MAX_ADDRESS_VERSION_BUFF_32 u6) - ;; Values for stacks address versions (define-constant STACKS_ADDR_VERSION_MAINNET 0x16) (define-constant STACKS_ADDR_VERSION_TESTNET 0x1a) @@ -116,8 +102,9 @@ ;; relative to BTC for this term. ;; Represented in basis points. min-ustx-ratio: uint, - ;; The allowed early unlock signers for this bond period - early-unlock-signers: (buff 683), + ;; The OP_ELSE (early-exit) subscript of the L1 lockup witness + ;; script for this bond period. + early-unlock-bytes: (buff 683), ;; The Stacks principal that can announce early L1 unlocks early-unlock-admin: principal, } @@ -239,25 +226,6 @@ (optional uint) ) -;; State for tracking used signer key authorizations. This prevents re-use -;; of the same signature or pre-set authorization for multiple transactions. -;; Refer to the `signer-key-authorizations` map for the documentation on these fields -(define-map used-signer-key-authorizations - { - signer-key: (buff 33), - reward-cycle: uint, - period: uint, - topic: (string-ascii 14), - pox-addr: (optional { - version: (buff 1), - hashbytes: (buff 32), - }), - auth-id: uint, - max-amount: uint, - } - bool ;; Whether the field has been used or not -) - ;; State to track the per-share rewards earned for bond periods ;; and reward cycles. This value must only increment (define-map rewards-per-token-for-cycle @@ -272,8 +240,8 @@ ;; bond or stx-only cycle (define-map total-shares-staked-for-cycle { - index: uint, is-bond: bool, + index: uint, } uint ) @@ -281,8 +249,8 @@ ;; State to track the per-staker shares for a given signer. (define-map staker-shares-staked-for-cycle { - index: uint, is-bond: bool, + index: uint, staker: principal, signer: principal, } @@ -290,20 +258,21 @@ ) ;; Amount of shares staked for a given signer in a given cycle. -;; This is strictly for reward calculations - ie the STX -;; from Bitcoin staking are not accounted for here. +;; This is strictly for reward calculations - +;; i.e. when is-bond is false, only the STX from STX-only staking +;; is accounted for here, not the STX from bonds. (define-map signer-shares-staked-for-cycle { - index: uint, is-bond: bool, + index: uint, signer: principal, } uint ) ;; Represents a snapshot of `rewards-per-token` at the last -;; time of rewards calculation for this specific signer -(define-map signer-rewards-per-token-paid-for-cycle +;; time of rewards settlement for this specific signer +(define-map signer-rewards-per-token-settled-for-cycle { is-bond: bool, index: uint, @@ -313,7 +282,7 @@ ) ;; Represents pending, but unclaimed rewards for a signer -(define-map signer-pending-rewards-for-cycle +(define-map signer-unclaimed-rewards-for-cycle { is-bond: bool, index: uint, @@ -331,7 +300,6 @@ ;; Data vars that store a copy of the burnchain configuration. ;; Implemented as data-vars, so that different configurations can be ;; used in e.g. test harnesses. -;; #[allow(unused_data_var)] (define-data-var pox-prepare-cycle-length uint (if is-in-mainnet u100 u50 @@ -348,12 +316,6 @@ ;; The first reward cycle where the first bond period occurs (define-data-var first-bond-period-cycle uint u0) -;;;; The last accounted balance (of sBTC held by this contract) -;;;; at a time of reward computation. -;;;; N.B. it is critical that this value is set to the contract's -;;;; sBTC balance after any transfer of sBTC out of this contract. -;; (define-data-var last-accounted-balance uint u0) - ;; The last accounted balance of rewards. Used to keep ;; track of which sBTC is just for rewards, vs from ;; staking. @@ -414,20 +376,26 @@ ;; Setup a new protocol bond by providing parameters and the ;; allowlist for the bond. ;; +;; @param bond-index; the index of the bond to set up ;; @param target-rate; target yield rate (apy) in basis points ;; @param stx-value-ratio; representation of STX:BTC price ;; @param min-ustx-ratio; minimum amount of STX that must be locked ;; relative to BTC for this term. Represented in basis points. -;; @param early-exit-signers: An allowlist of bond members that can -;; participate in the bond. +;; @param early-unlock-bytes: Bitcoin script that will be used to validate +;; early exit from the bond. It should be of the form +;; ` OP_CHECKSIGVERIFY` or an M-of-N `CHECKMULTISIGVERIFY` template. +;; @param early-unlock-admin: The principal that will be allowed to announce +;; early exits from the bond. +;; @param allowlist: A list of allowed stakers and their maximum sats that can +;; be staked for this bond. ;; -;; This function can only be called once. +;; This function can only be called once for each bond. (define-public (setup-bond (bond-index uint) (target-rate uint) (stx-value-ratio uint) (min-ustx-ratio uint) - (early-unlock-signers (buff 683)) + (early-unlock-bytes (buff 683)) (early-unlock-admin principal) (allowlist (list 1000 { staker: principal, @@ -465,7 +433,7 @@ target-rate: target-rate, stx-value-ratio: stx-value-ratio, min-ustx-ratio: min-ustx-ratio, - early-unlock-signers: early-unlock-signers, + early-unlock-bytes: early-unlock-bytes, early-unlock-admin: early-unlock-admin, }) ERR_BOND_ALREADY_SETUP @@ -482,7 +450,7 @@ target-rate: target-rate, stx-value-ratio: stx-value-ratio, min-ustx-ratio: min-ustx-ratio, - early-unlock-signers: early-unlock-signers, + early-unlock-bytes: early-unlock-bytes, max-allocation-sats: (get sum-max-sats accumulator), }) ) @@ -576,8 +544,8 @@ (bond-start-height (bond-period-to-burn-height bond-index)) ;; the first cycle in which their stx are unlocked (unlock-cycle (+ first-reward-cycle BOND_LENGTH_CYCLES)) - (current-total-staked (get-total-shares-staked-for-cycle bond-index true)) - (current-signer-staked (get-signer-shares-staked-for-cycle signer bond-index true)) + (current-total-staked (get-total-shares-staked-for-cycle true bond-index)) + (current-signer-staked (get-signer-shares-staked-for-cycle signer true bond-index)) ) ;; Verify that they're sending enough STX (asserts! @@ -605,7 +573,7 @@ ;; The signer must have been registered already (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) - ;;;; must be called directly by the tx-sender or by an allowed contract-caller + ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) (asserts! (is-none (get-bond-membership tx-sender)) @@ -621,7 +589,7 @@ (map-set protocol-bonds-total-staked bond-index (+ current-total-staked sats-total) ) - (crystallize-rewards signer bond-index true) + (settle-rewards signer true bond-index) (map-set total-shares-staked-for-cycle { index: bond-index, is-bond: true, @@ -677,14 +645,14 @@ (current-membership (unwrap! (get-bond-membership tx-sender) ERR_NOT_BOND_PARTICIPANT)) (current-signer (get signer current-membership)) (bond-index (get bond-index current-membership)) - (amount-sats (get-staker-shares-staked-for-cycle tx-sender bond-index true + (amount-sats (get-staker-shares-staked-for-cycle tx-sender true bond-index current-signer )) (bond-start-cycle (bond-period-to-reward-cycle bond-index)) (bond-end-cycle (bond-period-to-reward-cycle (+ bond-index u6))) (next-cycle (+ (current-pox-reward-cycle) u1)) - (current-signer-total-sats (get-signer-shares-staked-for-cycle current-signer bond-index true)) - (new-signer-total-sats (get-signer-shares-staked-for-cycle signer bond-index true)) + (current-signer-total-sats (get-signer-shares-staked-for-cycle current-signer true bond-index)) + (new-signer-total-sats (get-signer-shares-staked-for-cycle signer true bond-index)) ;; If the bond hasn't started yet, then the first cycle where ;; this new signer is active is the start cycle. Otherwise, it's the next reward ;; cycle. In other words, `max(bond-start-cycle, current-cycle + 1)` @@ -722,8 +690,8 @@ ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) - (crystallize-rewards current-signer bond-index true) - (crystallize-rewards signer bond-index true) + (settle-rewards current-signer true bond-index) + (settle-rewards signer true bond-index) ;; Remove the staker from all existing cycles (try! (remove-staker-from-cycles tx-sender first-reward-cycle num-cycles false)) @@ -821,7 +789,7 @@ ;; The signer must have been registered already (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) - ;; the start-burn-ht must result in the next reward cycle, do not allow stackers + ;; the start-burn-ht must result in the next reward cycle, do not allow stakers ;; to "post-date" their transaction (asserts! (is-eq first-reward-cycle specified-reward-cycle) ERR_INVALID_START_BURN_HEIGHT @@ -830,16 +798,16 @@ ;; lock period must be in acceptable range. (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) - ;;;; must be called directly by the tx-sender or by an allowed contract-caller + ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) ;; Cannot be already staked (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) - ;;;; tx-sender principal must not be in a bond membership + ;; tx-sender principal must not be in a bond membership (asserts! (is-none (get-bond-membership tx-sender)) ERR_ALREADY_STAKED) - ;;;; the Stacker must have sufficient unlocked funds + ;; the Staker must have sufficient unlocked funds (asserts! (>= (stx-get-balance tx-sender) amount-ustx) ERR_INSUFFICIENT_STX ) @@ -907,7 +875,7 @@ ;; lock period must be in acceptable range. (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) - ;;;; must be called directly by the tx-sender or by an allowed contract-caller + ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) ;; Must have enough unlocked STX @@ -965,9 +933,9 @@ (bond-index (get bond-index membership)) (signer (get signer membership)) (bond (unwrap-panic (get-protocol-bond bond-index))) - (amount-sats (get-staker-shares-staked-for-cycle staker bond-index true signer)) - (current-total-shares (get-total-shares-staked-for-cycle bond-index true)) - (current-shares (get-signer-shares-staked-for-cycle signer bond-index true)) + (amount-sats (get-staker-shares-staked-for-cycle staker true bond-index signer)) + (current-total-shares (get-total-shares-staked-for-cycle true bond-index)) + (current-shares (get-signer-shares-staked-for-cycle signer true bond-index)) ) ;; Only the early unlock admin for this bond period can call this function. ;; Calling via other contracts is not allowed. @@ -987,7 +955,7 @@ err-val true ) - (crystallize-rewards signer bond-index true) + (settle-rewards signer true bond-index) (map-set staker-shares-staked-for-cycle { is-bond: true, @@ -1027,9 +995,9 @@ )) (bond-index (get bond-index membership)) (signer (get signer membership)) - (current-amount-sats (get-staker-shares-staked-for-cycle staker bond-index true signer)) - (current-total-shares (get-total-shares-staked-for-cycle bond-index true)) - (current-shares (get-signer-shares-staked-for-cycle signer bond-index true)) + (current-amount-sats (get-staker-shares-staked-for-cycle staker true bond-index signer)) + (current-total-shares (get-total-shares-staked-for-cycle true bond-index)) + (current-shares (get-signer-shares-staked-for-cycle signer true bond-index)) (current-total-sbtc-staked (get-total-sbtc-staked)) ;; Cannot withdrawal more than they've staked (new-amount-sats (try! (if (<= amount-to-withdrawal-sats current-amount-sats) @@ -1059,7 +1027,7 @@ ) ;; Take a snapshot of the signer's current rewards - (crystallize-rewards signer bond-index true) + (settle-rewards signer true bond-index) (map-set staker-shares-staked-for-cycle { is-bond: true, @@ -1117,7 +1085,7 @@ (asserts! (is-eq old-signer (get signer current-info)) ERR_INVALID_OLD_SIGNER_MANAGER ) - ;;;; must be called directly by the tx-sender or by an allowed contract-caller + ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) ;; do not allow during a prepare phase @@ -1157,7 +1125,7 @@ ) ) -;;;; Remove a staker from a signer for X cycles +;; Remove a staker from a signer for X cycles (define-private (remove-staker-from-cycles (staker principal) (first-reward-cycle uint) @@ -1215,9 +1183,9 @@ ;; staking) to this signer. (cur-delegated-for-signer (get-amount-delegated-for-signer signer cycle)) ;; uSTX staked for this signer (through STX-only staking) - (cur-staked-for-signer (get-signer-shares-staked-for-cycle signer cycle false)) + (cur-staked-for-signer (get-signer-shares-staked-for-cycle signer false cycle)) ;; Total uSTX staked (through stx-only staking) this cycle - (total-shares-staked (get-total-shares-staked-for-cycle cycle false)) + (total-shares-staked (get-total-shares-staked-for-cycle false cycle)) (amount (get amount-ustx membership)) (is-stx-staking (get is-stx-staking accumulator)) (stake-amount (if is-stx-staking @@ -1228,7 +1196,7 @@ (is-in-signer-set (is-some (get-signer-set-item-for-cycle signer cycle))) ) ;; Crystallize STX-only rewards before mutating anything - (crystallize-rewards signer cycle false) + (settle-rewards signer false cycle) (if is-in-signer-set (if (< new-delegated SIGNER_SET_MIN_USTX) ;; They've crossed back below the threshold - remove from the signer set @@ -1372,11 +1340,11 @@ )) (staker (get staker accumulator)) (prev-staked (get-signer-pending-staked-ustx-per-cycle signer cycle)) - (prev-total-shares-staked (get-total-shares-staked-for-cycle cycle false)) + (prev-total-shares-staked (get-total-shares-staked-for-cycle false cycle)) (new-delegated (+ cur-delegated-for-signer amount)) ) ;; Crystallize STX-only rewards before mutating anything - (crystallize-rewards signer cycle false) + (settle-rewards signer false cycle) (if (>= new-delegated SIGNER_SET_MIN_USTX) (begin (map-set signer-shares-staked-for-cycle { @@ -1485,7 +1453,7 @@ (bond (unwrap! (get-protocol-bond bond-index) ERR_BOND_NOT_FOUND)) (expected-timelock-output (construct-lockup-output-script staker (get-bond-l1-unlock-height bond-index) - (get unlock-bytes lockups) (get early-unlock-signers bond) + (get unlock-bytes lockups) (get early-unlock-bytes bond) )) (accumulation (try! (fold validate-l1-lockup (get outputs lockups) (ok { @@ -1521,9 +1489,7 @@ (accumulator (try! accumulator-res)) (block (try! (parse-block-header (get header lockup)))) (expected-script-hash (get expected-script-hash accumulator)) - (output (try! (get-bitcoin-tx-output? (get tx lockup) (get output-index lockup) - (get amount lockup) expected-script-hash - ))) + (output (try! (get-bitcoin-tx-output? (get tx lockup) (get output-index lockup)))) (reversed-txid (get txid output)) (txid (reverse-buff32 reversed-txid)) ) @@ -1533,6 +1499,9 @@ (asserts! (is-eq (get script output) expected-script-hash) ERR_INVALID_LOCKUP_SCRIPT ) + (asserts! (is-eq (get amount output) (get amount lockup)) + ERR_INVALID_LOCKUP_AMOUNT + ) ;; verify merkle proof (asserts! (or @@ -1608,15 +1577,21 @@ (new-reserve (/ (* remaining-rewards RESERVE_RATIO) u10000)) (stx-staker-rewards (- remaining-rewards new-reserve)) (stx-cycle (burn-height-to-reward-cycle calculation-height)) - (cycle-staked-ustx (get-total-shares-staked-for-cycle stx-cycle false)) - (current-rewards-per-ustx (get-rewards-per-token-for-cycle stx-cycle false)) + (cycle-staked-ustx (get-total-shares-staked-for-cycle false stx-cycle)) + (current-rewards-per-ustx (get-rewards-per-token-for-cycle false stx-cycle)) (prev-accounted-rewards (var-get last-accounted-rewards-only)) - (new-rewards-per-ustx (if (is-eq cycle-staked-ustx u0) - ;; if there are no stx staked, we have a problem + ;; If no STX is staked this cycle, the staker cut will be applied to the reserve. + (no-stx-stakers (is-eq cycle-staked-ustx u0)) + (new-rewards-per-ustx (if no-stx-stakers u0 (/ (* stx-staker-rewards PRECISION) cycle-staked-ustx) )) (next-rewards-per-ustx (+ current-rewards-per-ustx new-rewards-per-ustx)) + ;; When no STX is staked, fold the staker cut into the reserve, otherwise zero. + (stranded-staker-cut (if no-stx-stakers + stx-staker-rewards + u0 + )) ) (print { topic: "calculate-rewards", @@ -1628,8 +1603,11 @@ stx-cycle: stx-cycle, cycle-staked-ustx: cycle-staked-ustx, next-rewards-per-ustx: next-rewards-per-ustx, + stranded-staker-cut: stranded-staker-cut, }) - (var-set reserve-balance (+ cur-reserve new-reserve)) + (var-set reserve-balance + (+ cur-reserve new-reserve stranded-staker-cut) + ) (var-set last-reward-compute-height calculation-height) (var-set last-accounted-rewards-only (+ prev-accounted-rewards (- accrued-rewards new-reserve)) @@ -1663,11 +1641,11 @@ (let ( (accumulator (try! accumulator-res)) (bond (unwrap! (map-get? protocol-bonds bond-index) ERR_BOND_NOT_FOUND)) - (total-sats (get-total-shares-staked-for-cycle bond-index true)) + (total-sats (get-total-shares-staked-for-cycle true bond-index)) (available-rewards (get available-rewards accumulator)) ;; How much sBTC the bond is supposed to earn per calculation, - ;; which is (totalSats * apy) / 48 - (target-yield (/ (/ (* total-sats (get target-rate bond)) u10000) u48)) + ;; which is (totalSats * apy) / 50 + (target-yield (/ (/ (* total-sats (get target-rate bond)) u10000) u50)) ;; If there is enough to cover the target yield, use that. Otherwise, ;; this bond gets the remaining rewards. (earned (if (>= available-rewards target-yield) @@ -1675,7 +1653,7 @@ available-rewards )) (stx-value-ratio (get stx-value-ratio bond)) - (current-rewards-per-token (get-rewards-per-token-for-cycle bond-index true)) + (current-rewards-per-token (get-rewards-per-token-for-cycle true bond-index)) ;; Prevent divide-by-zero (new-rewards-per-token (if (is-eq total-sats u0) u0 @@ -1742,14 +1720,14 @@ ;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` (define-read-only (get-earned (signer principal) - (index uint) (is-bond bool) + (index uint) ) (let ( - (shares (get-signer-shares-staked-for-cycle signer index is-bond)) - (rpt-current (get-rewards-per-token-for-cycle index is-bond)) - (rpt-paid (get-signer-rewards-per-token-paid-for-cycle signer index is-bond)) - (pending (get-signer-pending-rewards-for-cycle signer index is-bond)) + (shares (get-signer-shares-staked-for-cycle signer is-bond index)) + (rpt-current (get-rewards-per-token-for-cycle is-bond index)) + (rpt-paid (get-signer-rewards-per-token-settled-for-cycle signer is-bond index)) + (pending (get-signer-unclaimed-rewards-for-cycle signer is-bond index)) (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) ) (+ pending newly-earned) @@ -1762,7 +1740,7 @@ ) (let ( (signer contract-caller) - (stx-rewards (update-claimable-rewards signer reward-cycle false)) + (stx-rewards (update-claimable-rewards signer false reward-cycle)) (bond-rewards (fold update-claimable-bond-rewards bond-periods { signer: signer, total: u0, @@ -1808,13 +1786,13 @@ ;; Returns the newly claimable amount. Does NOT transfer funds out. (define-private (update-claimable-rewards (signer principal) - (index uint) (is-bond bool) + (index uint) ) - (let ((earned (crystallize-rewards signer index is-bond))) + (let ((earned (settle-rewards signer is-bond index))) ;; After crystallization, all earnings live in pending. ;; Zero out pending since we're about to pay it. - (map-set signer-pending-rewards-for-cycle { + (map-set signer-unclaimed-rewards-for-cycle { is-bond: is-bond, index: index, signer: signer, @@ -1839,7 +1817,7 @@ ), }) ) - (let ((rewards-info (update-claimable-rewards (get signer accumulator) bond-index true))) + (let ((rewards-info (update-claimable-rewards (get signer accumulator) true bond-index))) { signer: (get signer accumulator), total: (+ (get total accumulator) (get earned rewards-info)), @@ -1856,23 +1834,23 @@ ;; ;; This MUST be called before any update to `signer-shares-staked-for-cycle`, ;; because changes to that state will effect rewards calculations. -(define-private (crystallize-rewards +(define-private (settle-rewards (signer principal) - (index uint) (is-bond bool) + (index uint) ) (let ( - (earned (get-earned signer index is-bond)) - (rewards-per-token (get-rewards-per-token-for-cycle index is-bond)) + (earned (get-earned signer is-bond index)) + (rewards-per-token (get-rewards-per-token-for-cycle is-bond index)) ) - (map-set signer-pending-rewards-for-cycle { + (map-set signer-unclaimed-rewards-for-cycle { is-bond: is-bond, index: index, signer: signer, } earned ) - (map-set signer-rewards-per-token-paid-for-cycle { + (map-set signer-rewards-per-token-settled-for-cycle { is-bond: is-bond, index: index, signer: signer, @@ -2269,38 +2247,38 @@ ) (define-read-only (get-rewards-per-token-for-cycle - (index uint) (is-bond bool) + (index uint) ) (default-to u0 (map-get? rewards-per-token-for-cycle { - index: index, is-bond: is-bond, + index: index, }) ) ) (define-read-only (get-total-shares-staked-for-cycle - (index uint) (is-bond bool) + (index uint) ) (default-to u0 (map-get? total-shares-staked-for-cycle { - index: index, is-bond: is-bond, + index: index, }) ) ) (define-read-only (get-signer-shares-staked-for-cycle (signer principal) - (index uint) (is-bond bool) + (index uint) ) (default-to u0 (map-get? signer-shares-staked-for-cycle { - index: index, is-bond: is-bond, + index: index, signer: signer, }) ) @@ -2309,8 +2287,8 @@ ;; Get the amount of shares staked for a given staker in a certain cycle. (define-read-only (get-staker-shares-staked-for-cycle (staker principal) - (index uint) (is-bond bool) + (index uint) (signer principal) ) (default-to u0 @@ -2323,30 +2301,30 @@ ) ) -(define-read-only (get-signer-rewards-per-token-paid-for-cycle +(define-read-only (get-signer-rewards-per-token-settled-for-cycle (signer principal) - (index uint) (is-bond bool) + (index uint) ) (default-to u0 - (map-get? signer-rewards-per-token-paid-for-cycle { + (map-get? signer-rewards-per-token-settled-for-cycle { signer: signer, - index: index, is-bond: is-bond, + index: index, }) ) ) -(define-read-only (get-signer-pending-rewards-for-cycle +(define-read-only (get-signer-unclaimed-rewards-for-cycle (signer principal) - (index uint) (is-bond bool) + (index uint) ) (default-to u0 - (map-get? signer-pending-rewards-for-cycle { + (map-get? signer-unclaimed-rewards-for-cycle { signer: signer, - index: index, is-bond: is-bond, + index: index, }) ) ) @@ -2408,6 +2386,10 @@ ) ) +(define-read-only (get-first-pox-5-reward-cycle) + (var-get first-pox-5-reward-cycle) +) + ;;; Contract caller allowances (define-read-only (check-caller-allowed) @@ -2659,7 +2641,7 @@ ) ) -;;;; Clarity-Bitcoin helpers +;;; Clarity-Bitcoin helpers ;; Parse a Bitcoin block header. ;; Returns a tuple structured as folowed on success: @@ -2766,17 +2748,12 @@ ;; Verify that a block header hashes to a burnchain header hash at a given height. ;; Returns true if so; false if not. -;; -;; TODO: remove the `is-in-mainnet` check, instead use proper mocks (define-read-only (verify-block-header (headerbuff (buff 80)) (expected-block-height uint) ) (match (get-burn-block-info? header-hash expected-block-height) - bhh (if is-in-mainnet - (is-eq bhh (reverse-buff32 (sha256 (sha256 headerbuff)))) - true - ) + bhh (is-eq bhh (reverse-buff32 (sha256 (sha256 headerbuff)))) false ) ) @@ -2787,47 +2764,29 @@ (sha256 (sha256 tx)) ) -;; TODO: replace with clarity built-ins -(define-private (verify-merkle-proof - ;; #[allow(unused_binding)] - (leaf-hash (buff 32)) - ;; #[allow(unused_binding)] - (root-hash (buff 32)) - ;; #[allow(unused_binding)] - (tx-index uint) - ;; #[allow(unused_binding)] - (tx-count uint) - ;; #[allow(unused_binding)] - (leaf-hashes (list 14 (buff 32))) - ) - true -) - -;; TODO: replace with clarity built-ins -(define-private (get-bitcoin-tx-output? - (tx-bytes (buff 100000)) - ;; #[allow(unused_binding)] - (output-index uint) - ;; TODO: remove when built-in exists - ;; #[allow(unused_binding)] - (amount uint) - ;; TODO: remove when built-in exists - ;; #[allow(unused_binding)] - (script (buff 34)) - ) - (if true - (ok { - amount: amount, - script: script, - txid: (get-reversed-txid tx-bytes), - }) - (err u1) ;; indeterminate type otherwise - ) -) - ;;; Lock script helpers -;; Contruct an L1 lockup script +;; Contruct an L1 lockup script. +;; +;; `unlock-bytes` and `early-unlock-bytes` are caller-supplied Bitcoin +;; Script *subscripts*. `unlock-bytes` should be a subscript that validates the +;; signature of the staker (e.g., ` OP_CHECKSIG` or an M-of-N +;; `CHECKMULTISIG` template). It MUST leave a valid result on the stack. +;; `early-unlock-bytes` should be a subscript that validates the signature of +;; the early unlock admin and MUST NOT leave anything on the stack (e.g. +;; ` OP_CHECKSIGVERIFY`, or an M-of-N `CHECKMULTISIGVERIFY` template). +;; +;; The constructed script has this structure: +;; ``` +;; OP_DROP +;; OP_IF +;; OP_CHECKLOCKTIMEVERIFY OP_DROP +;; +;; OP_ELSE +;; +;; +;; OP_ENDIF +;; ``` (define-read-only (construct-lockup-script (staker principal) (unlock-burn-height uint) @@ -2838,10 +2797,10 @@ (concat 0x7563 ;; OP_DROP, OP_IF (concat (push-c-script-num unlock-burn-height) (concat 0xb175 ;; OP_CHECKLOCKTIMEVERIFY, OP_DROP - (concat (push-script-bytes unlock-bytes) + (concat unlock-bytes (concat 0x67 ;; OP_ELSE - (concat (push-script-bytes early-unlock-bytes) - (concat (push-script-bytes unlock-bytes) 0x68 + (concat early-unlock-bytes + (concat unlock-bytes 0x68 ;; OP_ENDIF )) )) @@ -2864,17 +2823,24 @@ ) ;; Convert a u8 or u16 to a little-endian byte buffer, -;; ONLY FOR n < 0xffff +;; ONLY FOR n <= 0xffff (or it will panic). (define-read-only (uint-to-buff-le (n uint)) - (unwrap-panic (as-max-len? - (unwrap-panic (slice? (unwrap-panic (to-consensus-buff? n)) + (let ( + (bounds-check_ (unwrap-panic (if (<= n u65535) + (some true) + none + ))) + (bytes (unwrap-panic (to-consensus-buff? n))) + (lsb (unwrap-panic (slice? bytes u16 u17))) + ) + (unwrap-panic (as-max-len? (if (< n u256) - u16 - u17 - ) u17 + lsb + (concat lsb (unwrap-panic (slice? bytes u15 u16))) + ) + u2 )) - u2 - )) + ) ) ;; Construct the correct script for pushing bytes into a Bitcoin script. diff --git a/stacking/deployments/default.simnet-plan.yaml b/stacking/deployments/default.simnet-plan.yaml index d6dccfb..dd3125d 100644 --- a/stacking/deployments/default.simnet-plan.yaml +++ b/stacking/deployments/default.simnet-plan.yaml @@ -88,10 +88,10 @@ plan: contract-name: pox-5 emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM path: contracts/pox-5.clar - clarity-version: 4 + clarity-version: 6 - transaction-type: emulated-contract-publish contract-name: pox-5-signer emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM path: contracts/pox-5-signer.clar - clarity-version: 4 - epoch: '3.3' + clarity-version: 6 + epoch: '4.0' diff --git a/stacking/package.json b/stacking/package.json index 7e26b50..a404630 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -12,12 +12,13 @@ "author": "", "license": "ISC", "dependencies": { - "@clarigen/core": "^4.1.5", + "@clarigen/core": "^4.1.6", "@noble/curves": "^2.0.1", "@scure/base": "^1.2.0", "@scure/btc-signer": "^1.5.0", "@stacks/api": "6.11.4-pr.472091f.0", "@stacks/blockchain-api-client": "8.15.1", + "@stacks/clarinet-sdk": "^3.18.1", "@stacks/common": "7.3.1", "@stacks/encryption": "7.4.0", "@stacks/network": "7.3.1", @@ -29,7 +30,7 @@ "pino-pretty": "^10.3.1" }, "devDependencies": { - "@clarigen/cli": "^4.1.5", + "@clarigen/cli": "^4.1.6", "@dotenvx/dotenvx": "^0.26.0", "@stacks/prettier-config": "^0.0.10", "@total-typescript/tsconfig": "^1.0.4", diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index 7820c2f..a9ff7f7 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -19,6 +19,8 @@ microblock_frequency = 1000 # mine_microblocks = true # max_microblocks = 10 pox_5_sbtc_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-token" +pox_5_sbtc_registry_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-registry" +pox_5_bond_admin = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP" [miner] first_attempt_time_ms = 180_000 From 6ee6e905eb75e3262b465720d95af5ea7c67e672 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:21:41 -0700 Subject: [PATCH 16/30] feat: update stacks-core commit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index fecd67c..8785901 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 84314f08b7b046d113696a0406d081b55ec74e28 # feat/epoch-4-rc + - &STACKS_BLOCKCHAIN_COMMIT 6970f0cc02fc1e9b9ee1340918d477b13cfa0edc # feat/epoch-4-rc - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za From fae01a94cebed2fd32014e8c3ef1f82be1ba8b19 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:44:32 -0700 Subject: [PATCH 17/30] fix: update read_only read_length limit --- stacks-krypton-miner.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index a9ff7f7..e7e1d68 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -39,6 +39,7 @@ disable_inbound_handshakes = false disable_inbound_walks = false public_ip_address = "1.1.1.1:1234" auth_token = "12345" +read_only_call_limit_read_length = 10000000000 # Add stacks-api as an event observer [[events_observer]] From 3211386e0e153a3add87e272a0fe6b2f2d7fcece Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:40:09 -0700 Subject: [PATCH 18/30] fix: update stacks-blockchain-api image --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8785901..f508dba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ x-common-vars: - &STACKS_BLOCKCHAIN_COMMIT 6970f0cc02fc1e9b9ee1340918d477b13cfa0edc # feat/epoch-4-rc - - &STACKS_API_COMMIT fc47a7cd054018d8ea098fbb3c8db207dbf7c8c9 # 9.0.0-pox5.1 + - &STACKS_API_COMMIT 8cb53b51b4bfb6db29295cfc7d4fe84c40944718 # 9.0.0-pox5.1 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za - &BITCOIN_PEER_PORT 18444 From 270c2b05454c4833ea19a3bbeb3e55e0513d82bc Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:58:46 -0700 Subject: [PATCH 19/30] fix: off-by-1 issue with stake-update timing --- stacking/btc-staker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index c669cec..d1e60c8 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -256,7 +256,7 @@ async function run() { const unlockCycle = burnBlockToRewardCycle(account.unlockHeight); - if (unlockCycle === nowCycle) { + if (unlockCycle === nowCycle + 1) { account.logger.info( { unlockHeight: account.unlockHeight, nowCycle, unlockCycle }, 'Extending stake...' From b967b61f3d76d2539239d3aec983d338208c2cb6 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Fri, 5 Jun 2026 05:51:10 -0700 Subject: [PATCH 20/30] feat: update bond admin --- stacks-krypton-miner.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index e7e1d68..21b879a 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -20,7 +20,8 @@ microblock_frequency = 1000 # max_microblocks = 10 pox_5_sbtc_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-token" pox_5_sbtc_registry_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-registry" -pox_5_bond_admin = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP" +# Shared bond admin account +pox_5_bond_admin = "ST1V2ASRWGR81W7GBN1Z4W2JQKXJWCADPVZG30X45" [miner] first_attempt_time_ms = 180_000 @@ -206,3 +207,7 @@ address = "STT8DSJTWAW9TVJ1B17SD3S6F7SYH4TXG7TWS7Q9" amount = 10000000000000000 # privateKey = 16226f674796712dfbd53bf402304579b8b6d04d4bed4d466bf84ce6db973d4401 # mnemonic = "essay grief twin tube concert idea prosper good alarm goddess shell glare hurt belt endless patch lumber wrap labor body erupt brown style test" + +[[ustx_balance]] +address = "ST1V2ASRWGR81W7GBN1Z4W2JQKXJWCADPVZG30X45" +amount = 10000000000000000 \ No newline at end of file From fec4f19c19f0f70d25f251222d8e3bfdcac8409c Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:27:24 -0700 Subject: [PATCH 21/30] fix: use CHAIN_ID --- stacking/pox-5-helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stacking/pox-5-helpers.ts b/stacking/pox-5-helpers.ts index 4245d24..f2ac2aa 100644 --- a/stacking/pox-5-helpers.ts +++ b/stacking/pox-5-helpers.ts @@ -15,7 +15,7 @@ import { } from '@clarigen/core'; import { contracts, project } from './clarigen-types.js'; import { sha256 } from '@noble/hashes/sha2.js'; -import { network } from './common.js'; +import { CHAIN_ID, network } from './common.js'; export const clarigenClient = new ClarigenClient(network); @@ -73,7 +73,7 @@ export function signSignerKeyGrant({ domain: Cl.tuple({ name: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.name), version: Cl.stringAscii(pox5.constants.pOX_5_SIGNER_DOMAIN.version), - 'chain-id': Cl.uint(pox5.constants.pOX_5_SIGNER_DOMAIN.chainId), + 'chain-id': Cl.uint(CHAIN_ID), }), }); const data = signWithKey(signerSk, hex.encode(sha256(fullMessage))); From bb7f3ad2b2e177f5641b52c4b5717fecf13ee4a4 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:32:28 -0700 Subject: [PATCH 22/30] feat: added sBTC containers --- docker-compose.yml | 143 +++++++++++++++ sbtc/signer/signer-config.toml | 315 +++++++++++++++++++++++++++++++++ 2 files changed, 458 insertions(+) create mode 100644 sbtc/signer/signer-config.toml diff --git a/docker-compose.yml b/docker-compose.yml index f508dba..5cb3df7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,33 @@ x-common-vars: # - &STACKS_CHAIN_ID ${STACKS_CHAIN_ID:-0x80000100} - &CUSTOM_CHAIN_IDS ${CUSTOM_CHAIN_IDS:-testnet=0x55005500,mainnet=12345678,mainnet=0xdeadbeaf,testnet=0x80000100} +x-sbtc-postgres: &sbtc-postgres + networks: + - stacks + image: postgres:16.6-bookworm@sha256:c965017e1d29eb03e18a11abc25f5e3cd78cb5ac799d495922264b8489d5a3a1 + stop_grace_period: 5s + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: signer + +x-sbtc-signer: &sbtc-signer + networks: + - stacks + image: ghcr.io/stacks-sbtc/sbtc:signer-v1.3.3@sha256:955a9ee0d28466c0fc2c88c7ba74f69c7121e8679ff7142469da552e565421b7 + entrypoint: + - /usr/local/bin/signer + - --config + - /signer-config.toml + - --migrate-db + - --output-format + - json + environment: &sbtc-signer-environment + RUST_LOG: info,signer=debug + SIGNER_SIGNER__P2P__LISTEN_ON: tcp://0.0.0.0:4122 + volumes: + - ./sbtc/signer/signer-config.toml:/signer-config.toml + services: bitcoind: networks: @@ -367,6 +394,122 @@ services: set -e exec ./node_modules/.bin/tsx /root/btc-staker.ts + emily-dynamodb: + networks: + - stacks + image: amazon/dynamodb-local:latest + command: -jar DynamoDBLocal.jar -sharedDb -dbPath . + ports: + - "127.0.0.1:8000:8000" + + emily-aws-setup: + networks: + - stacks + image: djordon/emily-aws-setup:v1.3.3@sha256:efe9dcf1dc3a9e9beb4c39f741e6bb2ff4a76c7adfc1bad19ba34e04b7035d7d + depends_on: + - emily-dynamodb + environment: + DYNAMODB_ENDPOINT: http://emily-dynamodb:8000 + DEPLOYER_ADDRESS: SN3R84XZYA63QS28932XQF3G1J8R9PC3W76P9CSQS + + emily-server: + networks: + - stacks + image: djordon/emily-server:v1.3.3@sha256:0c0986662fd8fbdfd5eaa27c3307cafe481a1e0ddb8edabc82cf4cd6e277e451 + depends_on: + emily-aws-setup: + condition: service_completed_successfully + environment: + DYNAMODB_ENDPOINT: http://emily-dynamodb:8000 + AWS_ACCESS_KEY_ID: xxxxxxxxxxxx + AWS_SECRET_ACCESS_KEY: xxxxxxxxxxxx + AWS_REGION: us-west-2 + PORT: 3031 + DEFAULT_PEG_CAP: 100000000000 + DEFAULT_PER_DEPOSIT_CAP: 100000000 + DEFAULT_PER_WITHDRAWAL_CAP: 100000000 + DEFAULT_ROLLING_WITHDRAWAL_BLOCKS: 144 + DEFAULT_ROLLING_WITHDRAWAL_CAP: 100000000000 + DEPLOYER_ADDRESS: SN3R84XZYA63QS28932XQF3G1J8R9PC3W76P9CSQS + ports: + - "127.0.0.1:3031:3031" + + emily-sidecar: + networks: + - stacks + image: djordon/emily-sidecar:v1.3.3@sha256:d1271d06f456ea86ca33c724bf58db54670fb9179a36eb0eac567eadc771ef21 + restart: on-failure + depends_on: + - emily-server + environment: + EMILY_API_KEY: testApiKey + EMILY_ENDPOINT: http://emily-server:3031 + ports: + - "127.0.0.1:20540:20540" + + sbtc-postgres-1: + <<: *sbtc-postgres + container_name: sbtc-postgres-1 + ports: + - "127.0.0.1:5432:5432" + + sbtc-signer-1: + <<: *sbtc-signer + container_name: sbtc-signer-1 + depends_on: + - sbtc-postgres-1 + - bitcoind + environment: + <<: *sbtc-signer-environment + SIGNER_SIGNER__DB_ENDPOINT: postgresql://postgres:postgres@sbtc-postgres-1:5432/signer + SIGNER_SIGNER__PRIVATE_KEY: 41634762d89dfa09133a4a8e9c1378d0161d29cd0a9433b51f1e3d32947a73dc + SIGNER_SIGNER__P2P__SEEDS: tcp://sbtc-signer-2:4122,tcp://sbtc-signer-3:4122 + SIGNER_SIGNER__PROMETHEUS_EXPORTER_ENDPOINT: 0.0.0.0:9181 + ports: + - "127.0.0.1:8801:8801" + + sbtc-postgres-2: + <<: *sbtc-postgres + container_name: sbtc-postgres-2 + ports: + - "127.0.0.1:5433:5432" + + sbtc-signer-2: + <<: *sbtc-signer + container_name: sbtc-signer-2 + depends_on: + - sbtc-postgres-2 + - bitcoind + environment: + <<: *sbtc-signer-environment + SIGNER_SIGNER__DB_ENDPOINT: postgresql://postgres:postgres@sbtc-postgres-2:5432/signer + SIGNER_SIGNER__PRIVATE_KEY: 9bfecf16c9c12792589dd2b843f850d5b89b81a04f8ab91c083bdf6709fbefee01 + SIGNER_SIGNER__P2P__SEEDS: tcp://sbtc-signer-1:4122,tcp://sbtc-signer-3:4122 + SIGNER_SIGNER__PROMETHEUS_EXPORTER_ENDPOINT: 0.0.0.0:9182 + ports: + - "127.0.0.1:8802:8801" + + sbtc-postgres-3: + <<: *sbtc-postgres + container_name: sbtc-postgres-3 + ports: + - "127.0.0.1:5434:5432" + + sbtc-signer-3: + <<: *sbtc-signer + container_name: sbtc-signer-3 + depends_on: + - sbtc-postgres-3 + - bitcoind + environment: + <<: *sbtc-signer-environment + SIGNER_SIGNER__DB_ENDPOINT: postgresql://postgres:postgres@sbtc-postgres-3:5432/signer + SIGNER_SIGNER__PRIVATE_KEY: 3ec0ca5770a356d6cd1a9bfcbf6cd151eb1bd85c388cc00648ec4ef5853fdb7401 + SIGNER_SIGNER__P2P__SEEDS: tcp://sbtc-signer-1:4122,tcp://sbtc-signer-2:4122 + SIGNER_SIGNER__PROMETHEUS_EXPORTER_ENDPOINT: 0.0.0.0:9183 + ports: + - "127.0.0.1:8803:8801" + monitor: networks: - stacks diff --git a/sbtc/signer/signer-config.toml b/sbtc/signer/signer-config.toml new file mode 100644 index 0000000..2e78b05 --- /dev/null +++ b/sbtc/signer/signer-config.toml @@ -0,0 +1,315 @@ +# !! ============================================================================== +# !! Blocklist Client Configuration +# !! ============================================================================== +# You may specify a blocklist client url. If one is not specified, then +# deposit or withdrawal requests are always accepted. +# +# Format: "http(s)://:" +# Default: +# Required: false +# Environment: SIGNER_BLOCKLIST_CLIENT__ENDPOINT +# [blocklist_client] +# endpoint = "http://127.0.0.1:8080" + +# The delay, in milliseconds, for the retry after a blocklist client failure +# +# Required: false +# Environment: SIGNER_BLOCKLIST_CLIENT__RETRY_DELAY +# retry_delay = 1000 + +# !! ============================================================================== +# !! Emily API Configuration +# !! ============================================================================== +[emily] +# The URI(s) of the Emily API server to connect to. +# +# You may specify multiple Emily API servers if you have them. They will be +# tried round-robin until one succeeds. +# +# Format: ["http(s)://[api-key@]:", ..] +# Default: +# Required: true +# Environment: SIGNER_EMILY__ENDPOINTS +# Environment Example: '"https://1234567890abcdef@api.emilyexample.com",..' +endpoints = [ + "http://testApiKey@emily-server:3031", +] + +# The pagination timeout, in seconds, used to fetch deposits requests from Emily. +# Required: false +# Environment: SIGNER_EMILY__PAGINATION_TIMEOUT +# pagination_timeout = 10 + +# !! ============================================================================== +# !! Bitcoin Core Configuration +# !! ============================================================================== +[bitcoin] +# The URI(s) of the Bitcoin Core RPC server(s) to connect to. +# +# You may specify multiple Bitcoin Core RPC servers if you have them. They will +# be randomly tried until one succeeds. +# +# Format: ["http://:@:", ..] +# Default: +# Required: true +# Environment: SIGNER_BITCOIN__RPC_ENDPOINTS +# Environment Example: http://user:pass@seed-1:4122,http://foo:bar@seed-2:4122 +rpc_endpoints = [ + "http://btc:btc@bitcoind:18443", +] + +# An optional fallback fee rate in sats/vbyte to use when the initial fee rate +# is too high to construct any transaction package. When set, this value is used +# directly as the retry fee rate. When unset, the signer estimates a lower fee +# rate by targeting a longer confirmation window. +# This is for tests only, it fails validation in mainnet. +# +# Default: +# Required: false +# Environment: SIGNER_BITCOIN__FALLBACK_FEE +fallback_fee = 5 + +# !! ============================================================================== +# !! Stacks Node Configuration +# !! ============================================================================== +[stacks] +# The RPC URL(s) of the Stacks node(s) to connect to. At least one must be +# provided. If multiple nodes are provided they will be tried in order when +# making requests. +endpoints = ["http://stacks-node:20443"] + +# !! ============================================================================== +# !! Signer Configuration +# !! ============================================================================== +[signer] +# The private key associated with the signer. This is used to generate the +# signers associated public key and sign messages to other signers. +# +# This may be either in 32- or 33-byte format. If you generated the key using +# `stacks-cli` or other ecosystem tools, it is likely that the key is in 33-byte +# format which includes a stacks-proprietary suffix byte. The sBTC signer doesn't +# make use of this byte and it will be trimmed automatically if provided. +# +# Format: "" (64 or 66 hex-characters) +# Required: true +# Environment: SIGNER_SIGNER__PRIVATE_KEY +private_key = "" + +# Specifies which network to use when constructing and sending transactions +# on stacks and bitcoin. This corresponds to the `chain` flag in the +# bitcoin.conf file of the connected bitcoin-core node, and the +# `burnchain.mode` flag int he config.toml of the connected stacks-core +# node. +# +# Required: true +# Possible values: mainnet, testnet, regtest +# Environment: SIGNER_SIGNER__NETWORK +network = "regtest" + +# Seconds to wait before processing a new Bitcoin block. +# Required: true +# Environment: SIGNER_SIGNER__BITCOIN_PROCESSING_DELAY +bitcoin_processing_delay = 3 + +# Seconds to wait before processing new SBTC requests. +# Required: true +# Environment: SIGNER_SIGNER__REQUESTS_PROCESSING_DELAY +requests_processing_delay = 0 + +# How many bitcoin blocks back from the chain tip the signer will look for +# requests. Must be strictly positive. +# +# Required: false +# Environment: SIGNER_SIGNER__CONTEXT_WINDOW +# context_window = 500 + +# The maximum amount of time, in seconds, a signing round will take before +# the coordinator will time out and return an error. This value must be +# strictly positive. +# +# Required: false +# Environment: SIGNER_SIGNER__SIGNER_ROUND_MAX_DURATION +# signer_round_max_duration = 30 + +# The maximum amount of time, in seconds, a coordinator will wait for +# pre-sign ACKs before timing out. Must be strictly +# positive. +# +# Required: false +# Environment: SIGNER_SIGNER__BITCOIN_PRESIGN_REQUEST_MAX_DURATION +# bitcoin_presign_request_max_duration = 30 + +# The maximum amount of time, in seconds, for a distributed key generation +# round before the coordinator will time out and return an error. Must be +# strictly positive. +# +# Required: false +# Environment: SIGNER_SIGNER__DKG_MAX_DURATION +# dkg_max_duration = 120 + +# The amount of time, in seconds, the signer should pause for after +# receiving a DKG begin message before relaying to give the other signers. +# +# Required: false +# Environment: SIGNER_SIGNER__DKG_BEGIN_PAUSE +dkg_begin_pause = 2 + +# The minimum bitcoin block height for which the sbtc signers will backfill +# bitcoin blocks to. The signers may not work if operated before this +# height. Defaults to the Nakamoto start height returned from the stacks +# node if not present. +# +# Required: false +# Environment: SIGNER_SIGNER__SBTC_BITCOIN_START_HEIGHT +# sbtc_bitcoin_start_height = 231 + +# The maximum number of deposit inputs that will be included in a single +# bitcoin transaction. +# +# Transactions must be constructed within a tenure of a bitcoin block, and +# higher values here imply lower likelihood of signing all inputs before +# the next bitcoin block arrives. +# +# Required: false +# Environment: SIGNER_SIGNER__MAX_DEPOSITS_PER_BITCOIN_TX +# max_deposits_per_bitcoin_tx = 25 + +# When defined, this field sets the scrape endpoint as an IPv4 or IPv6 +# socket address for exporting metrics for Prometheus. +# +# Required: false +# Environment: SIGNER_SIGNER__PROMETHEUS_EXPORTER_ENDPOINT +# prometheus_exporter_endpoint = "[::]:9184" + +# The address that deployed the sbtc smart contracts. +# +# Required: true +deployer = "SN3R84XZYA63QS28932XQF3G1J8R9PC3W76P9CSQS" + +# The signer database endpoint (pgsql connection string) +# +# Required: true +# Environment: SIGNER_SIGNER__DB_ENDPOINT +db_endpoint = "" + +# The public keys of the signer sit during the bootstrapping phase of +# the signers. +# +# Required: true +# Environment: SIGNER_SIGNER__BOOTSTRAP_SIGNING_SET +bootstrap_signing_set = [ + "035249137286c077ccee65ecc43e724b9b9e5a588e3d7f51e3b62f9624c2a49e46", + "031a4d9f4903da97498945a4e01a5023a1d53bc96ad670bfe03adf8a06c52e6380", + "02007311430123d4cad97f4f7e86e023b28143130a18099ecf094d36fef0f6135c", +] + +# The number of signatures required for the signers' bootstrapped +# multi-sig wallet on Stacks. +# +# Required: true +bootstrap_signatures_required = 2 + +# When defined, the signer will attempt to re-run DKG after the specified +# Bitcoin block height. Please only use this parameter when instructed to by +# the sBTC team. +# +# Required: false +# Environment: SIGNER_SIGNER__DKG_MIN_BITCOIN_BLOCK_HEIGHT +# dkg_min_bitcoin_block_height = 1234 + +# The number of bitcoin blocks after a DKG start where we attempt to verify the +# shares. After this many blocks, we mark the shares as failed. Please only use +# this parameter when instructed to by the sBTC team. +# +# Required: false +# Environment: SIGNER_SIGNER__DKG_VERIFICATION_WINDOW +# dkg_verification_window = 10 + +# !! ============================================================================== +# !! Stacks Event Observer Configuration +# !! +# !! The event observer listens for events on the Stacks blockchain. The listen +# !! address must be reachable by your Stacks node, and must be configured in the +# !! node's `event_observer` configuration section. +# !! +# !! Note that the event observer endpoint _does not_ support TLS and is served +# !! over HTTP. +# !! ============================================================================== +[signer.event_observer] +# The network interface (ip address) and port to bind the event observer server to. +# +# Format: ":" +# Required: true +# Environment: SIGNER_SIGNER__EVENT_OBSERVER__BIND +bind = "0.0.0.0:8801" + +# !! ============================================================================== +# !! Signer P2P Networking Configuration +# !! ============================================================================== +[signer.p2p] +# List of seed nodes to connect to bootstrap the network. +# +# If specified, these nodes will be used to discover other nodes on the network. +# If not specified or if none of the specified seeds could be reached, the node +# will attempt to discover other nodes using StackerDB. +# +# See the `listen_on` parameter for available protocols. +# +# Format: ["::", "::", ...] +# Required: false +# Environment: SIGNER_SIGNER__P2P__SEEDS +# Environment Example: tcp://seed-1:4122,tcp://seed-2:4122 +# TODO(429): Add well-known seed nodes +seeds = [] + +# The local network interface(s) and port(s) to listen on. +# +# You may specify multiple interfaces and ports by adding additional entries to +# the list. Entries can be addressed by any of IPv4 address, IPv6 address or +# hostname. Note that not all networks have IPv6 enabled, so it is recommended +# to provide an IPv4 address as well. +# +# Specifying a port of `0` will cause the server to bind to a random port, +# and an IP of `0.0.0.0` will cause the server to listen on all available +# interfaces. +# +# Available protocols: +# - tcp: Standard TCP socket connections. +# - quick-v1: QUIC over UDP. This protocol is faster and uses less bandwidth, +# but may not be supported by all nodes' networks. Nodes will always +# attempt QUIC connections first, and fall back to TCP if it fails. +# If UDP is blocked on your network then you should not specify a QUIC +# listener (as it will never be reachable). +# More information: https://en.wikipedia.org/wiki/QUIC +# +# Format: [":[:port]", ...] +# - If port is omitted then the default port 4122 will be used. +# Default: ["tcp://0.0.0.0:4122", "quic-v1://0.0.0.0:4122"] +# Required: false +# Environment: SIGNER_SIGNER__P2P__LISTEN_ON +listen_on = [] + +# The publicly accessible network endpoint to advertise to other nodes. +# +# If this is not specified then the node will attempt to use other peers on the +# network to determine its public endpoint. This is the recommended +# configuration for most users. +# +# If your network uses an advanced configuration with separate inbound/outbound +# addresses then you must specify this value with your inbound address and +# configure port-forwarding as auto-discovery will report your outbound address. +# +# Format: ["::", ...] (see `listen_on` for protocol options) +# Default: +# Required: true (but can be empty) +# Environment: SIGNER_SIGNER__P2P__PUBLIC_ENDPOINTS +public_endpoints = [] + +# Enables/disables mDNS (multicast DNS) discovery. mDNS allows sBTC signers +# running on the same local network to discover each other without explicitly +# providing them as seed nodes. +# +# Default: false +# Required: false +# Environment: SIGNER_SIGNER__P2P__ENABLE_MDNS +enable_mdns = false From 9fcb59b23ff00f752247e06fb8810afdb62384cc Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:51:33 -0700 Subject: [PATCH 23/30] feat: update config and scripts to use sbtc deployer addr --- docker-compose.yml | 3 +++ stacking/btc-staker.ts | 4 ++++ stacks-krypton-miner.toml | 9 +++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5cb3df7..7e7d5e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,7 @@ x-common-vars: - &STACKS_CHAIN_ID ${STACKS_CHAIN_ID:-0x80000000} # - &STACKS_CHAIN_ID ${STACKS_CHAIN_ID:-0x80000100} - &CUSTOM_CHAIN_IDS ${CUSTOM_CHAIN_IDS:-testnet=0x55005500,mainnet=12345678,mainnet=0xdeadbeaf,testnet=0x80000100} + - &SBTC_DEPLOYER_ADDRESS ${SBTC_DEPLOYER_ADDRESS:-SN3R84XZYA63QS28932XQF3G1J8R9PC3W76P9CSQS} x-sbtc-postgres: &sbtc-postgres networks: @@ -317,6 +318,7 @@ services: REWARD_RECIPIENT: *REWARD_RECIPIENT STACKS_CHAIN_ID: *STACKS_CHAIN_ID STACKS_LOG_JSON: 1 + SBTC_DEPLOYER_ADDRESS: *SBTC_DEPLOYER_ADDRESS entrypoint: - /bin/bash - -c @@ -384,6 +386,7 @@ services: BITCOIN_RPC_USER: *BITCOIN_RPC_USER BITCOIN_RPC_PASS: *BITCOIN_RPC_PASS BTC_LOCK_AMOUNT_SATS: 100000 + SBTC_DEPLOYER_ADDRESS: *SBTC_DEPLOYER_ADDRESS depends_on: - stacks-node - bitcoind diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index d1e60c8..02ec7c7 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -36,6 +36,7 @@ import { readFile } from 'node:fs/promises'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); const stakingCyclesPox5 = parseEnvInt('STACKING_CYCLES_POX_5', true); const lockAmountSats = BigInt(parseEnvInt('BTC_LOCK_AMOUNT_SATS', false) ?? 10_000_000); +const sbtcDeployerAddress = process.env.SBTC_DEPLOYER_ADDRESS!; let txFee = parseEnvInt('STACKING_FEE', false) ?? 1_000_000; const getNextTxFee = () => txFee++; @@ -195,6 +196,7 @@ async function run() { contractName: 'signer-manager', codeBody: signerManager .replaceAll(' .pox-5', ` '${pox5.identifier}`) + .replaceAll('SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4', sbtcDeployerAddress) .replaceAll( 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4', 'ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP' @@ -275,6 +277,8 @@ async function run() { } async function deploySBTC(account: Account) { + console.log('Skipping sBTC Deployment'); + return; const registry = await readFile( 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar', 'utf8' diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index 21b879a..743ae00 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -18,8 +18,8 @@ mine_microblocks = false microblock_frequency = 1000 # mine_microblocks = true # max_microblocks = 10 -pox_5_sbtc_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-token" -pox_5_sbtc_registry_contract = "ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP.sbtc-registry" +pox_5_sbtc_contract = "$SBTC_DEPLOYER_ADDRESS.sbtc-token" +pox_5_sbtc_registry_contract = "$SBTC_DEPLOYER_ADDRESS.sbtc-registry" # Shared bond admin account pox_5_bond_admin = "ST1V2ASRWGR81W7GBN1Z4W2JQKXJWCADPVZG30X45" @@ -210,4 +210,9 @@ amount = 10000000000000000 [[ustx_balance]] address = "ST1V2ASRWGR81W7GBN1Z4W2JQKXJWCADPVZG30X45" +amount = 10000000000000000 + +# sBTC deployer account +[[ustx_balance]] +address = "$SBTC_DEPLOYER_ADDRESS" amount = 10000000000000000 \ No newline at end of file From 6b823cde9f6ae0d3434df6add702289cf852fd18 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:14:10 -0700 Subject: [PATCH 24/30] feat: successfully make sBTC deposit tx --- stacking/btc-staker.ts | 64 ++++++++++++++++++++++++++++++++++++++++++ stacking/package.json | 3 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 02ec7c7..67c6559 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -27,11 +27,14 @@ import { calculateUnlockBurnHeight, getLockingAddress, createOrLoadWallet, + bitcoinRPC, + getRawTransaction, listUnspent, sendToAddress, } from './btc-helpers.js'; import { signSignerKeyGrant, pox5, pox5Signer, clarigenClient } from './pox-5-helpers.js'; import { readFile } from 'node:fs/promises'; +import { buildSbtcDepositAddress, REGTEST, SbtcApiClientDevenv } from 'sbtc'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); const stakingCyclesPox5 = parseEnvInt('STACKING_CYCLES_POX_5', true); @@ -41,6 +44,15 @@ const sbtcDeployerAddress = process.env.SBTC_DEPLOYER_ADDRESS!; let txFee = parseEnvInt('STACKING_FEE', false) ?? 1_000_000; const getNextTxFee = () => txFee++; +type BitcoinTxVerbose = { + vout: Array<{ + n: number; + scriptPubKey: { + address?: string; + }; + }>; +}; + // -- Initialization -- async function initBtcWallet() { @@ -146,6 +158,7 @@ async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockB // -- Main loop -- const grantedSignerKeys = new Set(); +const depositedSBTC = new Set(); let hasDeployedSBTC = false; async function run() { @@ -242,6 +255,11 @@ async function run() { grantedSignerKeys.add(account.signerManager); } + if (!depositedSBTC.has(account.stxAddress)) { + await depositSBTC(account); + depositedSBTC.add(account.stxAddress); + } + if (account.lockedAmount === 0n) { account.logger.info('Account unlocked, staking...', { account: account.index, @@ -276,6 +294,52 @@ async function run() { await Promise.all(txIdsToWait.map(waitForTxConfirmed)); } +async function depositSBTC(account: Account) { + console.log('Depositing sBTC for account:', account.stxAddress); + const client = new SbtcApiClientDevenv({ + sbtcContract: sbtcDeployerAddress, + btcApiUrl: 'http://bitcoind:18443', + stxApiUrl: 'http://stacks-api:3999', + sbtcApiUrl: 'http://emily-server:3031', + }); + // 1. Build the sBTC deposit address + const deposit = buildSbtcDepositAddress({ + stacksAddress: account.stxAddress, // the address to send/mint the sBTC to + signersPublicKey: await client.fetchSignersPublicKey(), // the aggregated public key of the signers + reclaimLockTime: 950, // default locktime for reclaiming failed deposits + reclaimPublicKey: account.pubKey.slice(0, 64), // public key for reclaiming failed deposits + network: REGTEST, + maxSignerFee: 1000, // max fee the signers can charge for processing the subsequent sweep tx + }); + + // console.log('Deposit Script:', deposit.depositScript); + // console.log('Reclaim Script:', deposit.reclaimScript); + // console.log('P2TR Output:', deposit.trOut); + console.log('Deposit Address:', { address: deposit.address, account: account.stxAddress }); + + const txid = await sendToAddress(WALLET_NAME, deposit.address, 0.1); + console.log('Sent BTC to deposit address:', { + txid: txid, + address: deposit.address, + account: account.stxAddress, + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + const transaction = await getRawTransaction(txid); + if (!/^[0-9a-f]+$/i.test(transaction)) { + throw new Error(`Expected raw transaction hex for ${txid}, got: ${transaction.slice(0, 80)}`); + } + const transactionInfo = await bitcoinRPC('getrawtransaction', [txid, true]); + const vout = transactionInfo.vout.find( + output => output.scriptPubKey.address === deposit.address + )?.n; + if (vout === undefined) { + throw new Error(`Could not find deposit output for ${deposit.address} in ${txid}`); + } + console.log('Transaction:', { transaction, vout }); + const notifyResult = await client.notifySbtc({ ...deposit, transaction, vout }); + console.log('Notified sbtc:', { notifyResult, txid }); +} + async function deploySBTC(account: Account) { console.log('Skipping sBTC Deployment'); return; diff --git a/stacking/package.json b/stacking/package.json index a404630..1dcf653 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -27,7 +27,8 @@ "@stacks/transactions": "7.4.0", "dotenv": "^16.4.5", "pino": "^8.19.0", - "pino-pretty": "^10.3.1" + "pino-pretty": "^10.3.1", + "sbtc": "^0.3.2" }, "devDependencies": { "@clarigen/cli": "^4.1.6", From 4cc57ef7962890a53c88bfa8bba23af90b0c9096 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:58:16 -0700 Subject: [PATCH 25/30] feat: update tsx for better tracing --- stacking/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stacking/package.json b/stacking/package.json index 1dcf653..fcfb22d 100644 --- a/stacking/package.json +++ b/stacking/package.json @@ -35,7 +35,7 @@ "@dotenvx/dotenvx": "^0.26.0", "@stacks/prettier-config": "^0.0.10", "@total-typescript/tsconfig": "^1.0.4", - "tsx": "4.7.1", + "tsx": "^4.22.4", "typescript": "^6.0.2", "vitest": "^4.1.3" }, From 4e10dc9d7de42eb7d7ad87794ab7403fd9387fad Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:58:33 -0700 Subject: [PATCH 26/30] feat: update stacks-core and stacks-blockchain-api --- docker-compose.yml | 6 +- stacking/Clarinet.toml | 1 + stacking/contracts/pox-5-signer.clar | 714 ++++++++--- stacking/contracts/pox-5.clar | 1742 +++++++++++++++++++------- 4 files changed, 1820 insertions(+), 643 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7e7d5e0..f367bb2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ x-common-vars: - - &STACKS_BLOCKCHAIN_COMMIT 6970f0cc02fc1e9b9ee1340918d477b13cfa0edc # feat/epoch-4-rc - - &STACKS_API_COMMIT 8cb53b51b4bfb6db29295cfc7d4fe84c40944718 # 9.0.0-pox5.1 + - &STACKS_BLOCKCHAIN_COMMIT 29ecd3621f2421e7e8f089cf376f1e4f5d018e7e # pox-wf-integration + - &STACKS_API_COMMIT 570a6bfffff30654df0f548f5857d1d17588c50c # pox5 - &BITCOIN_ADDRESSES miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT - &MINER_SEED 9e446f6b0c6a96cf2190e54bcd5a8569c3e386f091605499464389b8d4e0bfc201 # stx: STEW4ZNT093ZHK4NEQKX8QJGM2Y7WWJ2FQQS5C19, btc: miEJtNKa3ASpA19v5ZhvbKTEieYjLpzCYT, pub_key: 035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7c, wif: cStMQXkK5yTFGP3KbNXYQ3sJf2qwQiKrZwR9QJnksp32eKzef1za - &BITCOIN_PEER_PORT 18444 @@ -25,7 +25,7 @@ x-common-vars: - &STACKS_32_HEIGHT ${STACKS_32_HEIGHT:-133} - &STACKS_33_HEIGHT ${STACKS_33_HEIGHT:-134} - &STACKS_34_HEIGHT ${STACKS_34_HEIGHT:-135} - - &STACKS_40_HEIGHT ${STACKS_40_HEIGHT:-141} + - &STACKS_40_HEIGHT ${STACKS_40_HEIGHT:-142} - &STACKING_CYCLES ${STACKING_CYCLES:-1} # number of cycles to stack-stx or stack-extend for - &STACKING_CYCLES_POX_5 ${STACKING_CYCLES_POX_5:-1} # number of cycles to stack-stx or stack-extend for - &POX_PREPARE_LENGTH ${POX_PREPARE_LENGTH:-5} diff --git a/stacking/Clarinet.toml b/stacking/Clarinet.toml index 831ceff..d85f4b7 100644 --- a/stacking/Clarinet.toml +++ b/stacking/Clarinet.toml @@ -22,3 +22,4 @@ epoch = 4.0 unused_const = "warn" unused_data_var = "warn" panic = "off" +case_fn = "off" diff --git a/stacking/contracts/pox-5-signer.clar b/stacking/contracts/pox-5-signer.clar index 4e11a18..ab801c3 100644 --- a/stacking/contracts/pox-5-signer.clar +++ b/stacking/contracts/pox-5-signer.clar @@ -1,42 +1,119 @@ +;; Reference implementation for the signer manager trait, to be used with pox-5. +;; +;; This contract allows stakers to set a `pox-addr` that, when present, allows +;; rewards to be automatically withdrawn to BTC via an sBTC withdrawal. Anyone +;; can trigger this withdrawal, which allows for passively receiving L1 rewards. +;; +;; Admins of this contract can set fees. When fees are set, they are automatically +;; deducted from any stakers _newly calculated_ rewards. That means that if a staker +;; has not claimed or crystallized rewards in some amount of time, then a new fee +;; rate is set, the next time that staker claims rewards will have fees taken +;; from reward _even before_ the fee was set. + (impl-trait .pox-5.signer-manager-trait) (use-trait signer-manager-trait .pox-5.signer-manager-trait) +;; A staker tried to claim rewards, but they had none available (define-constant ERR_NO_CLAIMABLE_REWARDS (err u1001)) +;; Attempted to call an admin function +(define-constant ERR_UNAUTHORIZED_ADMIN (err u1002)) +;; the calldata provided when staking was invalid +(define-constant ERR_INVALID_CALLDATA (err u1003)) +;; The pox-addr provided as calldata isn't valid +(define-constant ERR_INVALID_POX_ADDR (err u1004)) +;; The fees provided when updating fees is invalid +(define-constant ERR_INVALID_FEES_BIPS (err u1005)) +;; A pox-5 callback (validate-stake!) was invoked by a +;; principal other than the pox-5 contract. +(define-constant ERR_UNAUTHORIZED_CALLER (err u1006)) +;; Attempted to withdraw more fees than have accrued. +(define-constant ERR_INSUFFICIENT_FEES (err u1007)) +;; The given withdrawal-request id is not tracked by this contract. +(define-constant ERR_UNKNOWN_WITHDRAWAL_REQUEST (err u1008)) +;; The withdrawal request has not been rejected, so its full +;; `amount + max-fee` is not reclaimable for the staker. +(define-constant ERR_WITHDRAWAL_NOT_REJECTED (err u1009)) +;; No refunds available to sweep. +(define-constant ERR_NO_REFUNDS (err u1010)) +;; The withdrawal request has not been accepted, so it cannot be +;; settled via `settle-accepted-withdrawal`. +(define-constant ERR_WITHDRAWAL_NOT_ACCEPTED (err u1011)) -;; Used to prevent fractional multiplication errors -;; during reward calculations -(define-constant PRECISION u1000000000000000000) ;; 1e18 +(define-constant MAX_BIPS u10000) -(define-map rewards-per-token-for-cycle - { - index: uint, - is-bond: bool, - } - uint +;; Maximum value of an address version as a uint +(define-constant MAX_ADDRESS_VERSION u6) +;; Maximum value of an address version that has a 20-byte hashbytes +;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes) +(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4) + +;; default to allowing deployer to register as a pool +(define-map admins + principal + bool ) +(map-set admins tx-sender true) + +;; Fees taken, in basis points, from rewards +(define-data-var fees-bips uint u0) + +;; Amount of earned fees that are held by the contract. +;; When fees are transferred out of the contract, this value +;; must be deducted. +(define-data-var earned-fees uint u0) -(define-map staker-rewards-per-token-settled-for-cycle +(define-map fee-bips-for-cycle { - is-bond: bool, - index: uint, - staker: principal, + reward-cycle: uint, + bond-index: (optional uint), } uint ) - -;; Represents pending, but unclaimed rewards for a staker -(define-map staker-unclaimed-rewards-for-cycle +;; When stakers provide L1 withdrawal info as calldata, +;; that is stored here. +(define-map pox-addrs + principal { - is-bond: bool, - index: uint, - staker: principal, + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + max-fee: uint, } +) + +;; Mapping of a given withdrawal request ID to the staker +;; whose rewards created that withdrawal. +(define-map withdrawal-requests uint + principal ) -;; #[allow(unnecessary_public)] +;; Sum of `amount + max-fee` over every live (un-settled) entry in +;; `withdrawal-requests`. Incremented when a withdrawal is initiated in +;; `claim-staker-rewards` and decremented when the request is settled +;; (`reclaim-failed-withdrawal` for rejected, `settle-accepted-withdrawal` for +;; accepted). This is staker-owed sBTC that has either left the contract balance +;; into the sBTC withdrawal system (pending) or been returned to the balance but +;; not yet paid out (rejected). `sweep-fee-refunds` subtracts it so an admin can +;; never sweep funds owed to a staker -- see the note on that function. +(define-data-var withdrawal-liability uint u0) + +;; sBTC pulled into this contract by `claim-rewards` that has not yet been paid +;; out to an individual staker via `claim-staker-rewards`. `claim-rewards` adds +;; the gross `total-rewards` it received; each `claim-staker-rewards` subtracts +;; that staker's `gross` as it is distributed (whether paid as sBTC, sent for +;; an L1 withdrawal, or retained as a signer-manager fee). Like +;; `withdrawal-liability`, this is subtracted in `sweep-fee-refunds` so an +;; admin can never sweep staker rewards. +(define-data-var unclaimed-staker-rewards uint u0) + +;; Callback function from a `stake` transaction. +;; +;; If `signer-calldata` is provided, then it must be in the form +;; of `{ version, hashbytes }` as a pox-addr. If provided, the pox-addr +;; is saved for the user, and they'll receive rewards through sBTC withdrawals. (define-public (validate-stake! - ;; #[allow(unused_binding)] (staker principal) ;; #[allow(unused_binding)] (first-index uint) @@ -48,242 +125,507 @@ (amount-sats uint) ;; #[allow(unused_binding)] (is-bond bool) - ;; #[allow(unused_binding)] (signer-calldata (optional (buff 500))) ) - (ok true) + (begin + (try! (authorize-pox-5)) + (ok (match signer-calldata + calldata + (let ((pox-addr (unwrap! + (from-consensus-buff? { + pox-addr: { + version: (buff 1), + hashbytes: (buff 32), + }, + max-fee: uint, + } + calldata + ) + ERR_INVALID_CALLDATA + ))) + (map-set pox-addrs staker pox-addr) + true + ) + ;; If `signer-calldata` is not provided, delete (if present) + ;; their entry from `pox-addrs`. + (map-delete pox-addrs staker) + )) + ) ) -(define-public (register-self - (signer-manager ) - (signer-key (buff 33)) - (auth-id uint) - (signer-sig (buff 65)) +;; Claim rewards _as the signer manager_ contract. When new rewards are available +;; from pox-5, this function must be called before rewards will be seen as available +;; to stakers of this signer. +;; +;; This function is callable by anyone. Once called, this contract will receive sBTC, +;; and rewards information will be crystallized. +(define-public (claim-rewards + (bond-periods (list 6 uint)) + (reward-cycle uint) ) - (as-contract? () - (try! (contract-call? .pox-5 grant-signer-key signer-key current-contract - auth-id signer-sig - )) - (try! (contract-call? .pox-5 register-signer signer-manager signer-key)) + (let ((result (try! (contract-call? .pox-5 claim-rewards bond-periods reward-cycle)))) + ;; The sBTC just pulled in is owed to this signer's stakers until each + ;; claims via `claim-staker-rewards`; reserve it so it is not sweepable. + (var-set unclaimed-staker-rewards + (+ (var-get unclaimed-staker-rewards) (get total-rewards result)) + ) + (map-insert fee-bips-for-cycle { + reward-cycle: reward-cycle, + bond-index: none, + } + (var-get fees-bips) + ) + (fold snapshot-bond-fee (get bond-rewards result) reward-cycle) + (ok result) ) ) -;; Handling rewards checkpointing for a staker -(define-public (checkpoint-staker +;;; Staker rewards + +;; Get the total amount of rewards earned since the last +;; rewards snapshot for this staker. Returns a tuple of `{ earned, fees }`. +;; The total portion of rewards the staker has accounted for +;; is `earned + fees`. +(define-read-only (get-earned-staker-rewards (staker principal) - (first-index uint) - (num-indexes uint) - (is-bond bool) + (reward-cycle uint) + (bond-index (optional uint)) ) - (begin - (try! (fold checkpoint-staker-for-index - (unwrap-panic (slice? - (list - u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11 u12 u13 u14 u15 - u16 u17 u18 u19 u20 u21 u22 u23 u24 u25 u26 u27 u28 u29 - u30 u31 u32 u33 u34 u35 u36 u37 u38 u39 u40 u41 u42 u43 - u44 u45 u46 u47 u48 u49 u50 u51 u52 u53 u54 u55 u56 u57 - u58 u59 u60 u61 u62 u63 u64 u65 u66 u67 u68 u69 u70 u71 - u72 u73 u74 u75 u76 u77 u78 u79 u80 u81 u82 u83 u84 u85 - u86 u87 u88 u89 u90 u91 u92 u93 u94 u95 + (let ( + (earned-before-fees (contract-call? .pox-5 get-earned-staker-rewards current-contract + reward-cycle bond-index staker + )) + (fees (/ + (* earned-before-fees + (get-fee-bips-for-cycle reward-cycle bond-index) ) - u0 num-indexes + MAX_BIPS )) - (ok { - staker: staker, - first-index: first-index, - is-bond: is-bond, - }) - )) - (ok true) + ) + { + earned: (- earned-before-fees fees), + fees: fees, + } ) ) -(define-private (checkpoint-staker-for-index - (index-offset uint) - (acc-res (response { - staker: principal, - first-index: uint, - is-bond: bool, - } - uint - )) +;; Trigger a claim of rewards for a given staker. +;; Anyone can call this function, and it will transfer rewards to the +;; staker. +;; +;; If the staker provided a `pox-addr` as calldata while staking, then +;; rewards are withdrawn through sBTC to their L1 Bitcoin address. Otherwise, +;; the staker receives sBTC. +(define-public (claim-staker-rewards + (staker principal) + (reward-cycle uint) + (bond-index (optional uint)) ) (let ( - (acc (try! acc-res)) - (staker (get staker acc)) - (index (+ (get first-index acc) index-offset)) + ;; `unwrap-panic` is ok here: there is no `err` type returnable + (rewards-info (unwrap-panic (contract-call? .pox-5 claim-staker-rewards-for-signer staker + reward-cycle bond-index + ))) + (prev-fees (var-get earned-fees)) + (gross (get earned rewards-info)) + (fees (/ (* gross (get-fee-bips-for-cycle reward-cycle bond-index)) + MAX_BIPS + )) + (earned (- gross fees)) + ) + (asserts! (> earned u0) ERR_NO_CLAIMABLE_REWARDS) + (asserts! + (> + (unwrap-panic (contract-call? + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + get-balance current-contract + )) + u0 + ) + ERR_NO_CLAIMABLE_REWARDS + ) + (var-set earned-fees (+ prev-fees fees)) + ;; This staker's share is being distributed now so release it from + ;; the unclaimed count recorded when `claim-rewards` pulled it in. + (var-set unclaimed-staker-rewards + (- (var-get unclaimed-staker-rewards) gross) ) - (settle-staker-rewards staker (get is-bond acc) index) - (ok acc) + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" earned + )) + (match (get-pox-addr staker) + l1-info (let ( + (amount (try! (if (>= earned (get max-fee l1-info)) + (ok (- earned (get max-fee l1-info))) + ERR_NO_CLAIMABLE_REWARDS + ))) + (withdrawal-request (try! (contract-call? + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal + initiate-withdrawal-request amount + (get pox-addr l1-info) (get max-fee l1-info) + ))) + ) + (print { + topic: "claim-staker-rewards", + amount-sats: earned, + l1-withdrawal: (some (merge l1-info { + withdrawal-request: withdrawal-request, + amount: amount, + })), + staker: staker, + reward-cycle: reward-cycle, + bond-index: bond-index, + }) + (map-set withdrawal-requests withdrawal-request staker) + ;; `amount + max-fee` == `earned` left the balance into the + ;; sBTC withdrawal system; record it as staker liability. + (var-set withdrawal-liability + (+ (var-get withdrawal-liability) + (+ amount (get max-fee l1-info)) + )) + true + ) + (begin + (print { + topic: "claim-staker-rewards", + amount-sats: earned, + l1-withdrawal: none, + staker: staker, + reward-cycle: reward-cycle, + bond-index: bond-index, + }) + (try! (contract-call? + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer earned tx-sender staker none + )) + ) + ))) + + (ok earned) ) ) -(define-private (settle-staker-rewards - (staker principal) - (is-bond bool) - (index uint) - ) +;; Reclaim a REJECTED L1 withdrawal back to the staker who earned it. +;; +;; `claim-staker-rewards` initiates the sBTC withdrawal inside `as-contract?`, +;; meaning this contract is the withdrawal's requester. Any sBTC the sBTC +;; protocol returns for that request therefore goes to this contract, not the +;; staker whose pox-5 balance was already zeroed. Two cases: +;; * REJECTED -> the full `amount + max-fee` is unlocked back to the +;; requester. Fully reclaimable for the staker on-chain. +;; * ACCEPTED -> only the unused fee budget (`max-fee - actual-fee`) is +;; minted back. The actual fee is not exposed by the sBTC +;; registry, so this dust cannot be attributed to a single +;; staker; it is recovered via `sweep-fee-refunds`. +;; +;; Permissionless, mirroring `claim-staker-rewards`: anyone may trigger it on a +;; staker's behalf. The `withdrawal-requests` entry is deleted so the reclaim +;; cannot be replayed. +(define-public (reclaim-failed-withdrawal (request-id uint)) (let ( - (earned (get-earned-staker-rewards staker is-bond index)) - (rewards-per-token (get-rewards-per-token-for-cycle is-bond index)) + (staker (unwrap! (map-get? withdrawal-requests request-id) + ERR_UNKNOWN_WITHDRAWAL_REQUEST + )) + (request (unwrap! + (contract-call? + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry + get-withdrawal-request request-id + ) + ERR_UNKNOWN_WITHDRAWAL_REQUEST + )) + (refund (+ (get amount request) (get max-fee request))) ) - (map-set staker-unclaimed-rewards-for-cycle { - staker: staker, - index: index, - is-bond: is-bond, - } - earned + ;; `status` is `none` while pending and `(some true)` once accepted; + ;; only `(some false)` (rejected) unlocks the full amount back here. + (asserts! (is-eq (get status request) (some false)) + ERR_WITHDRAWAL_NOT_REJECTED ) - (map-set staker-rewards-per-token-settled-for-cycle { + (map-delete withdrawal-requests request-id) + ;; Request is settled: drop it from the outstanding staker liability. + (var-set withdrawal-liability (- (var-get withdrawal-liability) refund)) + (print { + topic: "reclaim-failed-withdrawal", + request-id: request-id, staker: staker, - index: index, - is-bond: is-bond, - } - rewards-per-token + amount-sats: refund, + }) + (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" refund + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer refund tx-sender staker none + )) ) - { - earned: earned, - rewards-per-token: rewards-per-token, - } ) ) -(define-public (claim-rewards - (bond-periods (list 6 uint)) - (reward-cycle uint) - ) - (let ((new-rewards-info (try! (as-contract? () - (try! (contract-call? .pox-5 claim-rewards bond-periods reward-cycle)) - )))) - (update-rewards-info - (get rewards-per-token (get stx-rewards new-rewards-info)) false - reward-cycle +;; Settle an ACCEPTED L1 withdrawal. +;; +;; On acceptance the sBTC protocol pays the staker on L1 and mints only the +;; unused fee budget (`max-fee - actual-fee`) back to this contract as dust. No +;; staker payout is owed here, but the request is still counted in +;; `withdrawal-liability` (at its full `amount + max-fee`), which suppresses the +;; sweepable balance. This permissionless call retires the entry so that: +;; * its liability is released, and +;; * the accept-case dust it left behind becomes sweepable via +;; `sweep-fee-refunds`. +;; +;; Mirrors `reclaim-failed-withdrawal` (permissionless, deletes the entry to +;; prevent replay) but for the accept case, where there is nothing to pay out. +(define-public (settle-accepted-withdrawal (request-id uint)) + (let ( + (staker (unwrap! (map-get? withdrawal-requests request-id) + ERR_UNKNOWN_WITHDRAWAL_REQUEST + )) + (request (unwrap! + (contract-call? + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry + get-withdrawal-request request-id + ) + ERR_UNKNOWN_WITHDRAWAL_REQUEST + )) + (liability (+ (get amount request) (get max-fee request))) + ) + ;; `status` is `none` while pending and `(some false)` if rejected; + ;; only `(some true)` (accepted) is settleable here. Rejected requests + ;; must go through `reclaim-failed-withdrawal` so the staker is paid. + (asserts! (is-eq (get status request) (some true)) + ERR_WITHDRAWAL_NOT_ACCEPTED + ) + (map-delete withdrawal-requests request-id) + ;; Request is settled: drop it from the outstanding staker liability. + ;; The dust already minted to this contract stays in the balance and is + ;; now sweepable. + (var-set withdrawal-liability + (- (var-get withdrawal-liability) liability) ) - (fold update-bond-rewards-info (get bond-rewards new-rewards-info) true) - (ok new-rewards-info) + (print { + topic: "settle-accepted-withdrawal", + request-id: request-id, + staker: staker, + liability-released: liability, + }) + (ok true) ) ) -;; Get the total amount of rewards earned since the last -;; rewards snapshot for this staker. -;; -;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` -(define-read-only (get-earned-staker-rewards - (staker principal) - (is-bond bool) - (index uint) +;;; Admin functions + +;; Update the allowed admin principal +(define-public (update-admin + (admin principal) + (enabled bool) ) - (let ( - (shares (contract-call? .pox-5 get-staker-shares-staked-for-cycle staker - is-bond index current-contract - )) - (rpt-current (get-rewards-per-token-for-cycle is-bond index)) - (rpt-paid (get-staker-rewards-per-token-settled-for-cycle staker is-bond index)) - (pending (get-staker-unclaimed-rewards-for-cycle staker is-bond index)) - (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) - ) - (+ pending newly-earned) + (begin + (try! (authorize-admin)) + (print { + topic: "update-admin", + admin: admin, + enabled: enabled, + }) + (map-set admins admin enabled) + (ok admin) ) ) -(define-public (claim-staker-rewards - (is-bond bool) - (index uint) +;; Update the fees taken from rewards +(define-public (update-fees (new-fees uint)) + (begin + (try! (authorize-admin)) + (asserts! (<= new-fees MAX_BIPS) ERR_INVALID_FEES_BIPS) + (print { + topic: "update-fees", + old-fees: (var-get fees-bips), + new-fees: new-fees, + }) + (var-set fees-bips new-fees) + (ok true) ) +) + +;; Withdraw accrued admin fees from staker rewards. +(define-public (withdraw-fees + (amount uint) + (recipient principal) + ) + (let ((fees (var-get earned-fees))) + (try! (authorize-admin)) + (asserts! (<= amount fees) ERR_INSUFFICIENT_FEES) + (var-set earned-fees (- fees amount)) + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" amount + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount tx-sender recipient none + )) + )) + (ok amount) + ) +) + +;; Sweep orphaned sBTC fee-refund dust to a recipient. +;; +;; On an ACCEPTED withdrawal the sBTC protocol mints the unused fee budget +;; (`max-fee - actual-fee`) back to this contract. That dust cannot be +;; attributed to a specific staker on-chain (the sBTC registry does not expose +;; the actual fee paid), so it pools here; this admin-gated function sweeps it. +;; +;; The full sweepable amount is taken: the sBTC balance minus the fee +;; accumulator (`earned-fees`), the outstanding `withdrawal-liability`, and the +;; pooled `unclaimed-staker-rewards` that `claim-rewards` pulled in but no staker +;; has claimed yet, so it can NEVER sweep funds owed to a staker. A +;; rejected-but-unreclaimed withdrawal's `amount + max-fee` is present in BOTH +;; the sBTC balance (the protocol returned it here) and in +;; `withdrawal-liability` (the entry is still live), so the two cancel and the +;; refund stays untouchable, whether or not anyone has called +;; `reclaim-failed-withdrawal` yet. +;; +;; The flip side: while a withdrawal is pending, or accepted but not yet retired +;; via `settle-accepted-withdrawal`, its full `amount + max-fee` suppresses the +;; sweepable amount. To recover the accept-case fee dust an admin must first +;; `settle-accepted-withdrawal` the accepted requests (and wait for any pending +;; ones to finalize). +(define-public (sweep-fee-refunds (recipient principal)) (let ( - (staker tx-sender) - (rewards-info (settle-staker-rewards staker is-bond index)) - (earned (get earned rewards-info)) - ) - (asserts! (> earned u0) ERR_NO_CLAIMABLE_REWARDS) - (map-set staker-unclaimed-rewards-for-cycle { - staker: staker, - is-bond: is-bond, - index: index, - } - u0 + (balance (unwrap-panic (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + get-balance current-contract + ))) + (reserved (+ (var-get earned-fees) + (+ (var-get withdrawal-liability) + (var-get unclaimed-staker-rewards) + ))) + (sweepable (if (>= balance reserved) + (- balance reserved) + u0 + )) ) + (try! (authorize-admin)) + (asserts! (> sweepable u0) ERR_NO_REFUNDS) + (print { + topic: "sweep-fee-refunds", + amount-sats: sweepable, + recipient: recipient, + }) (try! (as-contract? ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token - "sbtc-token" earned + "sbtc-token" sweepable )) (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token - transfer earned tx-sender staker none + transfer sweepable tx-sender recipient none )) )) - (ok earned) + (ok sweepable) ) ) -(define-private (update-rewards-info - (rewards-per-share uint) - (is-bond bool) - (index uint) +;; As an admin, register this contract with a specific signer key. The signer key grant +;; must not have been used yet. +(define-public (register-self + (signer-manager ) + (signer-key (buff 33)) + (auth-id uint) + (signer-sig (buff 65)) ) (begin - (map-set rewards-per-token-for-cycle { - index: index, - is-bond: is-bond, - } - rewards-per-share - ) + (try! (authorize-admin)) + (try! (contract-call? .pox-5 grant-signer-key signer-key current-contract + auth-id signer-sig + )) + (contract-call? .pox-5 register-signer signer-manager signer-key) ) ) -(define-private (update-bond-rewards-info +(define-private (authorize-admin) + (ok (asserts! (and (is-eq contract-caller tx-sender) (is-admin tx-sender)) + ERR_UNAUTHORIZED_ADMIN + )) +) + +;; Ensure that the immediate caller is the pox-5 contract. The trait callbacks +;; (validate-stake!) write per-staker state keyed by the +;; `staker` argument; they must only ever be driven by pox-5, never invoked +;; directly by an external principal. +(define-private (authorize-pox-5) + (ok (asserts! (is-eq contract-caller .pox-5) ERR_UNAUTHORIZED_CALLER)) +) + +(define-read-only (is-admin (caller principal)) + (default-to false (map-get? admins caller)) +) + +(define-private (snapshot-bond-fee (bond-info { bond-index: uint, earned: uint, rewards-per-token: uint, }) ;; #[allow(unused_binding)] - (acc bool) + (reward-cycle uint) ) - (map-set rewards-per-token-for-cycle { - is-bond: true, - index: (get bond-index bond-info), - } - (get rewards-per-token bond-info) + (begin + (map-insert fee-bips-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some (get bond-index bond-info)), + } + (var-get fees-bips) + ) + reward-cycle ) ) -(define-read-only (get-rewards-per-token-for-cycle - (is-bond bool) - (index uint) +(define-read-only (get-fee-bips-for-cycle + (reward-cycle uint) + (bond-index (optional uint)) ) (default-to u0 - (map-get? rewards-per-token-for-cycle { - index: index, - is-bond: is-bond, + (map-get? fee-bips-for-cycle { + reward-cycle: reward-cycle, + bond-index: bond-index, }) ) ) -(define-read-only (get-staker-rewards-per-token-settled-for-cycle - (staker principal) - (is-bond bool) - (index uint) - ) - (default-to u0 - (map-get? staker-rewards-per-token-settled-for-cycle { - staker: staker, - index: index, - is-bond: is-bond, - }) - ) +(define-read-only (get-earned-fees) + (var-get earned-fees) ) -(define-read-only (get-staker-unclaimed-rewards-for-cycle - (staker principal) - (is-bond bool) - (index uint) - ) - (default-to u0 - (map-get? staker-unclaimed-rewards-for-cycle { - staker: staker, - index: index, - is-bond: is-bond, - }) +(define-read-only (get-withdrawal-liability) + (var-get withdrawal-liability) +) + +(define-read-only (get-unclaimed-staker-rewards) + (var-get unclaimed-staker-rewards) +) + +(define-read-only (get-pox-addr (staker principal)) + (map-get? pox-addrs staker) +) + +(define-read-only (get-withdrawal-request-staker (withdrawal-request uint)) + (map-get? withdrawal-requests withdrawal-request) +) + +(define-read-only (check-pox-addr (pox-addr { + version: (buff 1), + hashbytes: (buff 32), +})) + (let ( + (version (buff-to-uint-be (get version pox-addr))) + (expected-len (if (<= version MAX_ADDRESS_VERSION_BUFF_20) + u20 + u32 + )) + ) + (ok (asserts! + (and + (<= version MAX_ADDRESS_VERSION) + (is-eq (len (get hashbytes pox-addr)) expected-len) + (is-eq (len (get version pox-addr)) u1) + ) + ERR_INVALID_POX_ADDR + )) ) ) diff --git a/stacking/contracts/pox-5.clar b/stacking/contracts/pox-5.clar index 137cfc5..3f6648d 100644 --- a/stacking/contracts/pox-5.clar +++ b/stacking/contracts/pox-5.clar @@ -52,6 +52,20 @@ (define-constant ERR_UPDATE_BOND_SAME_SIGNER (err u44)) ;; The lockup amount does not match the specified amount of sats (define-constant ERR_INVALID_LOCKUP_AMOUNT (err u45)) +;; The same Bitcoin outpoint (txid + output-index) appeared twice in +;; the L1 lockup proof list submitted to `register-for-bond`. +(define-constant ERR_DUPLICATE_LOCKUP_OUTPOINT (err u46)) +;; A staker tried to modify the next reward cycle's state during the prepare +;; phase. +(define-constant ERR_STAKE_IN_PREPARE_PHASE (err u47)) +;; A staker tried to rollover a bond too early +(define-constant ERR_ROLLOVER_TOO_EARLY (err u48)) +;; A reentrant call into pox-5 was detected while a signer-manager call was in flight +(define-constant ERR_REENTRANT_CALL (err u49)) +;; The staker already announced an L1 early exit for this bond period +(define-constant ERR_L1_EARLY_EXIT_ALREADY_ANNOUNCED (err u50)) +;; A reserve withdrawal was attempted with insufficient reserve balance +(define-constant ERR_INSUFFICIENT_RESERVE_BALANCE (err u51)) ;; The length, in terms of staking cycles, of a given ;; bond period @@ -102,11 +116,9 @@ ;; relative to BTC for this term. ;; Represented in basis points. min-ustx-ratio: uint, - ;; The OP_ELSE (early-exit) subscript of the L1 lockup witness - ;; script for this bond period. + ;; The early-unlock subscript of the L1 lockup witness script for this + ;; bond period. early-unlock-bytes: (buff 683), - ;; The Stacks principal that can announce early L1 unlocks - early-unlock-admin: principal, } ) @@ -126,6 +138,7 @@ amount-ustx: uint, signer: principal, is-l1-lock: bool, + amount-sats: uint, } ) @@ -135,6 +148,16 @@ uint ) +;; Tracks whether a staker has announced their L1 early exit +;; for a given bond period. +(define-map protocol-bond-l1-early-exit-announced + { + bond-index: uint, + staker: principal, + } + bool +) + (define-map signer-key-grants { signer-key: (buff 33), @@ -230,8 +253,8 @@ ;; and reward cycles. This value must only increment (define-map rewards-per-token-for-cycle { - is-bond: bool, - index: uint, + reward-cycle: uint, + bond-index: (optional uint), } uint ) @@ -240,8 +263,8 @@ ;; bond or stx-only cycle (define-map total-shares-staked-for-cycle { - is-bond: bool, - index: uint, + reward-cycle: uint, + bond-index: (optional uint), } uint ) @@ -249,8 +272,8 @@ ;; State to track the per-staker shares for a given signer. (define-map staker-shares-staked-for-cycle { - is-bond: bool, - index: uint, + reward-cycle: uint, + bond-index: (optional uint), staker: principal, signer: principal, } @@ -263,8 +286,8 @@ ;; is accounted for here, not the STX from bonds. (define-map signer-shares-staked-for-cycle { - is-bond: bool, - index: uint, + reward-cycle: uint, + bond-index: (optional uint), signer: principal, } uint @@ -274,8 +297,8 @@ ;; time of rewards settlement for this specific signer (define-map signer-rewards-per-token-settled-for-cycle { - is-bond: bool, - index: uint, + reward-cycle: uint, + bond-index: (optional uint), signer: principal, } uint @@ -284,9 +307,41 @@ ;; Represents pending, but unclaimed rewards for a signer (define-map signer-unclaimed-rewards-for-cycle { - is-bond: bool, - index: uint, + reward-cycle: uint, + bond-index: (optional uint), + signer: principal, + } + uint +) + +;; Represents a snapshot of `rewards-per-token` at the last +;; time of rewards settlement for this specific staker +(define-map staker-rewards-per-token-settled-for-cycle + { + reward-cycle: uint, + bond-index: (optional uint), + signer: principal, + staker: principal, + } + uint +) + +;; Represents pending, but unclaimed rewards for a staker +(define-map staker-unclaimed-rewards-for-cycle + { + reward-cycle: uint, + bond-index: (optional uint), + signer: principal, + staker: principal, + } + uint +) + +(define-map signer-rewards-per-token-for-cycle + { signer: principal, + reward-cycle: uint, + bond-index: (optional uint), } uint ) @@ -330,19 +385,46 @@ ;; The total amount of sBTC staked (define-data-var total-sbtc-staked uint u0) +;; Reentrancy guard: prevents cross-function re-entry through signer-manager trait calls +(define-data-var signer-manager-call-active bool false) + (define-trait signer-manager-trait ( (validate-stake! ;; staker, first-index, num-indexes, amount-ustx, amount-sats, is-bond, signer-calldata (principal uint uint uint uint bool (optional (buff 500))) (response bool uint) ) - (checkpoint-staker - ;; staker, first-index, num-indexes, is-bond - (principal uint uint bool) - (response bool uint) - ) )) +(define-private (validate-no-reentrancy) + (ok (asserts! (not (var-get signer-manager-call-active)) ERR_REENTRANT_CALL)) +) + +;; A helper function to call the `validate-stake!` function on a given +;; signer-manager, wrapping the reentrancy guard logic around it. This should +;; be the only way that `validate-stake!` is called in the contract, since it +;; is critical to ensure that reentrancy attacks are prevented. +(define-private (signer-manager-validate-stake + (signer-manager ) + (staker principal) + (first-index uint) + (num-indexes uint) + (amount-ustx uint) + (amount-sats uint) + (is-bond bool) + (signer-calldata (optional (buff 500))) + ) + (begin + (asserts! (not (var-get signer-manager-call-active)) ERR_REENTRANT_CALL) + (var-set signer-manager-call-active true) + (try! (contract-call? signer-manager validate-stake! staker first-index + num-indexes amount-ustx amount-sats is-bond signer-calldata + )) + (var-set signer-manager-call-active false) + (ok true) + ) +) + ;; This function can only be called once, when it boots up (define-public (set-burnchain-parameters (first-burn-height uint) @@ -366,10 +448,20 @@ ) (define-public (set-bond-admin (new-admin principal)) - (begin + (let ( + (old-admin (var-get bond-admin)) + (result { + old-admin: old-admin, + new-admin: new-admin, + }) + ) ;; only bond admin can call this. - (asserts! (is-eq contract-caller (var-get bond-admin)) ERR_UNAUTHORIZED) - (ok (var-set bond-admin new-admin)) + (asserts! (is-eq contract-caller old-admin) ERR_UNAUTHORIZED) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + (var-set bond-admin new-admin) + (print (merge { topic: "set-bond-admin" } result)) + (ok result) ) ) @@ -381,11 +473,10 @@ ;; @param stx-value-ratio; representation of STX:BTC price ;; @param min-ustx-ratio; minimum amount of STX that must be locked ;; relative to BTC for this term. Represented in basis points. -;; @param early-unlock-bytes: Bitcoin script that will be used to validate -;; early exit from the bond. It should be of the form -;; ` OP_CHECKSIGVERIFY` or an M-of-N `CHECKMULTISIGVERIFY` template. -;; @param early-unlock-admin: The principal that will be allowed to announce -;; early exits from the bond. +;; @param early-unlock-bytes: Bitcoin script subscript that guards the +;; early-exit (OP_ELSE) branch of the L1 lockup. It should be of the form +;; ` OP_CHECKSIG` or an M-of-N `CHECKMULTISIG` template, and MUST +;; leave a valid result on the stack (it is consumed by the shared OP_VERIFY). ;; @param allowlist: A list of allowed stakers and their maximum sats that can ;; be staked for this bond. ;; @@ -396,16 +487,22 @@ (stx-value-ratio uint) (min-ustx-ratio uint) (early-unlock-bytes (buff 683)) - (early-unlock-admin principal) (allowlist (list 1000 { staker: principal, max-sats: uint, })) ) - (let ((bond-start-height (bond-period-to-burn-height bond-index))) + (let ( + (bond-start-height (bond-period-to-burn-height bond-index)) + (first-reward-cycle (bond-period-to-reward-cycle bond-index)) + (unlock-cycle (+ first-reward-cycle BOND_LENGTH_CYCLES)) + ) ;; only bond admin can call this. (asserts! (is-eq contract-caller (var-get bond-admin)) ERR_UNAUTHORIZED) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + ;; only can be called within 2 cycles of bond start (asserts! (or @@ -434,11 +531,23 @@ stx-value-ratio: stx-value-ratio, min-ustx-ratio: min-ustx-ratio, early-unlock-bytes: early-unlock-bytes, - early-unlock-admin: early-unlock-admin, }) ERR_BOND_ALREADY_SETUP ) + (print { + topic: "setup-bond", + bond-index: bond-index, + target-rate: target-rate, + stx-value-ratio: stx-value-ratio, + min-ustx-ratio: min-ustx-ratio, + early-unlock-bytes: early-unlock-bytes, + first-reward-cycle: first-reward-cycle, + bond-start-height: bond-start-height, + unlock-cycle: unlock-cycle, + unlock-burn-height: (reward-cycle-to-burn-height unlock-cycle), + }) + (let ((accumulator (try! (fold add-staker-to-bond allowlist (ok { sum-max-sats: u0, @@ -520,7 +629,7 @@ amount: uint, } ), - unlock-bytes: (buff 683), + staker-unlock-bytes: (buff 683), } uint )) @@ -528,10 +637,26 @@ ) (let ( (signer (contract-of signer-manager)) + ;; Compute the sats being staked for this bond. (sats-total (try! (match btc-lockup l1-lockups (verify-l1-lockups tx-sender bond-index l1-lockups) - sbtc-amount (lock-sbtc sbtc-amount) + sbtc-amount (ok sbtc-amount) ))) + ;; Any bond the staker is currently a member of. Some value here + ;; means this is a roll-over from an ending bond into a later one. + (existing-membership (map-get? protocol-bond-memberships tx-sender)) + ;; sBTC currently custodied for the staker's existing bond (0 if + ;; they have none, or if the existing bond is an L1 lock). + (old-sbtc (get-staker-custodied-sbtc tx-sender)) + ;; sBTC this new bond needs custodied (0 on the L1 path). + (new-sbtc (if (is-ok btc-lockup) + u0 + sats-total + )) + ;; Any STX-only stake the staker has. Present means this + ;; `register-for-bond` is a roll-over from an ending stx-only + ;; stake into a bond. + (existing-stake (map-get? staker-info tx-sender)) (bond (unwrap! (map-get? protocol-bonds bond-index) ERR_BOND_NOT_FOUND)) (allowance (unwrap! (map-get? protocol-bond-allowances { @@ -544,9 +669,13 @@ (bond-start-height (bond-period-to-burn-height bond-index)) ;; the first cycle in which their stx are unlocked (unlock-cycle (+ first-reward-cycle BOND_LENGTH_CYCLES)) - (current-total-staked (get-total-shares-staked-for-cycle true bond-index)) - (current-signer-staked (get-signer-shares-staked-for-cycle signer true bond-index)) + (current-total-staked (get-total-shares-staked-for-cycle first-reward-cycle + (some bond-index) + )) + (stx-balance (stx-account tx-sender)) + (total-balance (+ (get locked stx-balance) (get unlocked stx-balance))) ) + (try! (verify-not-prepare-phase)) ;; Verify that they're sending enough STX (asserts! (>= amount-ustx @@ -561,70 +690,121 @@ ERR_BOND_ALREADY_STARTED ) - ;; Cannot be already staked - (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) + ;; An existing STX-only stake is allowed only if its term ends no + ;; later than this bond's first reward cycle (no overlap). A stx-only + ;; stake has no L1 collateral, so there's no L1-unlock-window gate + ;; here -- the lock just extends forward via the node-side handler. + (asserts! + (match existing-stake + stake-info (<= + (+ (get first-reward-cycle stake-info) + (get num-cycles stake-info) + ) + first-reward-cycle + ) + true + ) + ERR_ALREADY_STAKED + ) + ;; Cannot stake more sats than their allowance (asserts! (<= sats-total allowance) ERR_TOO_MUCH_SATS) + ;; Must have enough unlocked STX + ;; the Staker must have sufficient total funds (locked + unlocked). + ;; On a roll-over the staker's STX is still locked by the ending + ;; bond; the node-side handler extends that lock to the new amount, + ;; so checking only `stx-get-balance` (unlocked) would falsely fail. + (asserts! (>= total-balance amount-ustx) ERR_INSUFFICIENT_STX) + ;; Validate that the staker can join this signer - (try! (contract-call? signer-manager validate-stake! tx-sender bond-index u1 + (try! (signer-manager-validate-stake signer-manager tx-sender bond-index u1 amount-ustx sats-total true signer-calldata )) - ;; The signer must have been registered already - (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; The signer must have been registered already, and its signer key + ;; grant must still be active. + (try! (verify-signer-key-grant signer + (unwrap! (get-signer-info signer) ERR_SIGNER_NOT_FOUND) + )) ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) - (asserts! (is-none (get-bond-membership tx-sender)) + ;; Reject if an existing membership *overlaps* this bond. An existing + ;; bond whose staking term ends no later than this bond's first cycle + ;; (e.g. rolling from bond N into bond N+6) is allowed. + (asserts! + (not (bond-overlaps-new-position? existing-membership first-reward-cycle)) ERR_ALREADY_REGISTERED ) + ;; Settle rewards before updating state + (settle-rewards signer first-reward-cycle (some bond-index)) + (settle-staker-rewards signer first-reward-cycle (some bond-index) + tx-sender + ) + + ;; A rollover from a non-overlapping existing bond may only happen in + ;; that bond's L1 unlock window, the last 1/2 cycle. + (try! (verify-bond-rollover-window existing-membership)) + + ;; Move the staker's custodied sBTC into this bond, transferring only the + ;; net difference vs. any bond they're rolling over from. + (try! (roll-sbtc tx-sender old-sbtc new-sbtc)) + (map-set protocol-bond-memberships tx-sender { bond-index: bond-index, amount-ustx: amount-ustx, signer: signer, is-l1-lock: (is-ok btc-lockup), + amount-sats: sats-total, }) (map-set protocol-bonds-total-staked bond-index (+ current-total-staked sats-total) ) - (settle-rewards signer true bond-index) - (map-set total-shares-staked-for-cycle { - index: bond-index, - is-bond: true, - } - (+ current-total-staked sats-total) - ) - (map-set signer-shares-staked-for-cycle { - index: bond-index, - is-bond: true, - signer: signer, - } - (+ current-signer-staked sats-total) - ) - (map-set staker-shares-staked-for-cycle { - index: bond-index, - is-bond: true, - staker: tx-sender, - signer: signer, - } - sats-total - ) + (try! (add-staker-to-bond-cycles tx-sender signer bond-index first-reward-cycle + BOND_LENGTH_CYCLES sats-total + )) (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle BOND_LENGTH_CYCLES amount-ustx false )) - (ok { - signer: signer, - staker: tx-sender, - amount-ustx: amount-ustx, - bond-index: bond-index, - first-reward-cycle: first-reward-cycle, - unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), - unlock-cycle: unlock-cycle, - }) + ;; If this was a roll-over from an STX-only stake, clear the + ;; staker-info entry so `stake-update` / `unstake` can no longer + ;; reach the now-stale stake. The stake's signer-cycle memberships + ;; through its original term stay intact (the staker keeps + ;; participating and earning through that term). + (if (is-some existing-stake) + (map-delete staker-info tx-sender) + true + ) + + (let ((result { + signer: signer, + staker: tx-sender, + amount-ustx: amount-ustx, + sats-total: sats-total, + bond-index: bond-index, + first-reward-cycle: first-reward-cycle, + unlock-burn-height: (reward-cycle-to-burn-height unlock-cycle), + unlock-cycle: unlock-cycle, + is-l1-lock: (is-ok btc-lockup), + btc-lockup: (match btc-lockup + l1-info { + type: "l1", + txs: (some (map get-l1-lockup-summary (get outputs l1-info))), + } + sbtc-amount { + type: "l2", + txs: none, + } + ), + })) + (print (merge { topic: "register-for-bond" } result)) + (ok result) + ) ) ) @@ -645,23 +825,20 @@ (current-membership (unwrap! (get-bond-membership tx-sender) ERR_NOT_BOND_PARTICIPANT)) (current-signer (get signer current-membership)) (bond-index (get bond-index current-membership)) - (amount-sats (get-staker-shares-staked-for-cycle tx-sender true bond-index - current-signer - )) + (current-cycle (current-pox-reward-cycle)) (bond-start-cycle (bond-period-to-reward-cycle bond-index)) (bond-end-cycle (bond-period-to-reward-cycle (+ bond-index u6))) - (next-cycle (+ (current-pox-reward-cycle) u1)) - (current-signer-total-sats (get-signer-shares-staked-for-cycle current-signer true bond-index)) - (new-signer-total-sats (get-signer-shares-staked-for-cycle signer true bond-index)) + (next-cycle (+ current-cycle u1)) ;; If the bond hasn't started yet, then the first cycle where ;; this new signer is active is the start cycle. Otherwise, it's the next reward - ;; cycle. In other words, `max(bond-start-cycle, current-cycle + 1)` - (first-reward-cycle (if (> bond-start-cycle next-cycle) - bond-start-cycle - next-cycle - )) + ;; cycle, unless the bond will be over at that point. + (first-reward-cycle (clamp next-cycle bond-start-cycle bond-end-cycle)) + (amount-sats (get amount-sats current-membership)) (num-cycles (- bond-end-cycle first-reward-cycle)) ) + (try! (verify-not-prepare-phase)) + + ;; Check that the old signer is the current signer (asserts! (is-eq old-signer current-signer) ERR_INVALID_OLD_SIGNER_MANAGER ) @@ -670,28 +847,27 @@ (asserts! (not (is-eq signer old-signer)) ERR_UPDATE_BOND_SAME_SIGNER) ;; Validate that the staker can join this signer - (try! (contract-call? signer-manager validate-stake! tx-sender bond-index u1 + (try! (signer-manager-validate-stake signer-manager tx-sender bond-index u1 (get amount-ustx current-membership) amount-sats true signer-calldata )) - ;; Call `old-signer-manager`, and allow them to snapshot current - ;; data before updating. Do not throw any errors. - (match (contract-call? old-signer-manager checkpoint-staker tx-sender bond-index - u1 true - ) - ok-val ok-val - err-val true - ) - - ;; The signer must have been registered already - (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + ;; The signer must have been registered already, and its signer key + ;; grant must still be active. + (try! (verify-signer-key-grant signer + (unwrap! (get-signer-info signer) ERR_SIGNER_NOT_FOUND) + )) ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) - (settle-rewards current-signer true bond-index) - (settle-rewards signer true bond-index) + ;; Settle rewards before mutating related state + (settle-rewards current-signer current-cycle (some bond-index)) + (settle-rewards signer current-cycle (some bond-index)) + (settle-staker-rewards current-signer current-cycle (some bond-index) + tx-sender + ) + (settle-staker-rewards signer current-cycle (some bond-index) tx-sender) ;; Remove the staker from all existing cycles (try! (remove-staker-from-cycles tx-sender first-reward-cycle num-cycles false)) @@ -702,44 +878,36 @@ )) ;; Remove the sBTC shares from the current signer - (map-delete staker-shares-staked-for-cycle { - index: bond-index, - staker: tx-sender, - signer: current-signer, - is-bond: true, - }) - (map-set signer-shares-staked-for-cycle { - index: bond-index, - is-bond: true, - signer: current-signer, - } - (- current-signer-total-sats amount-sats) - ) + (try! (remove-staker-from-bond-cycles tx-sender current-signer bond-index + first-reward-cycle num-cycles amount-sats + )) ;; Add the sBTC shares to the current signer - (map-set staker-shares-staked-for-cycle { - index: bond-index, - staker: tx-sender, - signer: signer, - is-bond: true, - } - amount-sats - ) - (map-set signer-shares-staked-for-cycle { - index: bond-index, - signer: signer, - is-bond: true, - } - (+ new-signer-total-sats amount-sats) - ) + (try! (add-staker-to-bond-cycles tx-sender signer bond-index first-reward-cycle + num-cycles amount-sats + )) (map-set protocol-bond-memberships tx-sender { bond-index: bond-index, amount-ustx: (get amount-ustx current-membership), signer: signer, is-l1-lock: (get is-l1-lock current-membership), + amount-sats: amount-sats, }) - (ok true) + (let ((result { + staker: tx-sender, + signer: signer, + old-signer: old-signer, + bond-index: bond-index, + amount-ustx: (get amount-ustx current-membership), + amount-sats: amount-sats, + first-reward-cycle: first-reward-cycle, + num-cycles: num-cycles, + is-l1-lock: (get is-l1-lock current-membership), + })) + (print (merge { topic: "update-bond-registration" } result)) + (ok result) + ) ) ) @@ -749,19 +917,27 @@ (signer-key (buff 33)) ) (let ((signer (contract-of signer-manager))) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + ;; Because signers can have members register at any time, ;; they must use signer key grants instead of per-tx ;; authorizations. (try! (verify-signer-key-grant signer signer-key)) ;; Only the signer contract itself can register itself - (asserts! (is-eq tx-sender signer) ERR_UNAUTHORIZED_SIGNER_REGISTRATION) + (asserts! (is-eq contract-caller signer) + ERR_UNAUTHORIZED_SIGNER_REGISTRATION + ) (map-set signers signer signer-key) - (ok { - signer: signer, - signer-key: signer-key, - }) + (let ((result { + signer: signer, + signer-key: signer-key, + })) + (print (merge { topic: "register-signer" } result)) + (ok result) + ) ) ) @@ -780,14 +956,30 @@ (specified-reward-cycle (+ u1 (burn-height-to-reward-cycle start-burn-ht))) ;; the first cycle in which their stx are unlocked (unlock-cycle (+ first-reward-cycle num-cycles)) - ) + ;; Any bond the staker is currently a member of. Some value here + ;; indicates this `stake` is a roll-over from an ending bond into + ;; STX-only. + (existing-membership (map-get? protocol-bond-memberships tx-sender)) + ;; sBTC currently custodied for the staker's existing bond (0 if + ;; they have none, or if the existing bond is an L1 lock). On a + ;; bond-to-stake rollover the full custody is refunded below. + (old-sbtc (get-staker-custodied-sbtc tx-sender)) + (stx-balance (stx-account tx-sender)) + (total-balance (+ (get locked stx-balance) (get unlocked stx-balance))) + ) + (try! (verify-not-prepare-phase)) + ;; Validate that the staker can join this signer - (try! (contract-call? signer-manager validate-stake! tx-sender + (try! (signer-manager-validate-stake signer-manager tx-sender first-reward-cycle num-cycles amount-ustx u0 false signer-calldata )) - ;; The signer must have been registered already - (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; The signer must have been registered already, and its signer key + ;; grant must still be active. + (try! (verify-signer-key-grant signer + (unwrap! (get-signer-info signer) ERR_SIGNER_NOT_FOUND) + )) ;; the start-burn-ht must result in the next reward cycle, do not allow stakers ;; to "post-date" their transaction @@ -801,17 +993,37 @@ ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) - ;; Cannot be already staked + ;; Cannot already be STX-only staking. Re-extending an existing stake + ;; goes through `stake-update`, not a second `stake` call. (asserts! (is-none (get-staker-info tx-sender)) ERR_ALREADY_STAKED) - ;; tx-sender principal must not be in a bond membership - (asserts! (is-none (get-bond-membership tx-sender)) ERR_ALREADY_STAKED) - - ;; the Staker must have sufficient unlocked funds - (asserts! (>= (stx-get-balance tx-sender) amount-ustx) - ERR_INSUFFICIENT_STX + ;; A roll-over from an existing bond is allowed when the bond's term + ;; ends no later than this stake's first reward cycle. Already-active + ;; bonds are rejected (overlap). Same shape as the + ;; `register-for-bond` gate. + (asserts! + (not (bond-overlaps-new-position? existing-membership first-reward-cycle)) + ERR_ALREADY_STAKED ) + ;; A roll-over from an ending bond may only happen once that bond's + ;; L1 collateral would have unlocked -- the same window an L1 bond + ;; holder has to redirect their BTC. Keeps parity with the + ;; `register-for-bond` gate so a bond's STX / sBTC can't be released + ;; ahead of the bond's L1 unlock height. + (try! (verify-bond-rollover-window existing-membership)) + + ;; the Staker must have sufficient total funds (locked + unlocked). + ;; On a roll-over the staker's STX is still locked by the ending + ;; bond; the node-side handler extends that lock to the new amount, + ;; so checking only `stx-get-balance` (unlocked) would falsely fail. + (asserts! (>= total-balance amount-ustx) ERR_INSUFFICIENT_STX) + + ;; Refund any sBTC custodied for the rolled-over bond (zero-target + ;; net transfer). No-op when there is no existing bond, or when the + ;; existing bond is an L1 lock. + (try! (roll-sbtc tx-sender old-sbtc u0)) + (try! (add-staker-to-signer-cycles tx-sender signer first-reward-cycle num-cycles amount-ustx true )) @@ -823,15 +1035,24 @@ signer: signer, }) - (ok { - signer: signer, - staker: tx-sender, - amount-ustx: amount-ustx, - num-cycle: num-cycles, - first-reward-cycle: first-reward-cycle, - unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), - unlock-cycle: unlock-cycle, - }) + ;; If this was a roll-over from a bond, clear the bond membership so + ;; `unstake-sbtc` / `update-bond-registration` can no longer reach + ;; the old bond. The old bond's reward shares stay through its term; + ;; only the management pointer is gone. + (map-delete protocol-bond-memberships tx-sender) + + (let ((result { + signer: signer, + staker: tx-sender, + amount-ustx: amount-ustx, + num-cycles: num-cycles, + first-reward-cycle: first-reward-cycle, + unlock-burn-height: (reward-cycle-to-burn-height unlock-cycle), + unlock-cycle: unlock-cycle, + })) + (print (merge { topic: "stake" } result)) + (ok result) + ) ) ) @@ -860,17 +1081,24 @@ (first-reward-cycle (+ current-cycle u1)) (num-cycles (- unlock-cycle current-cycle u1)) ) + (try! (verify-not-prepare-phase)) + ;; Validate that the staker can join this signer - (try! (contract-call? signer-manager validate-stake! tx-sender + (try! (signer-manager-validate-stake signer-manager tx-sender first-reward-cycle num-cycles new-lock-amount u0 false signer-calldata )) + ;; Validate that `old-signer-manager` matches their current signer (asserts! (is-eq old-signer (get signer current-info)) ERR_INVALID_OLD_SIGNER_MANAGER ) - ;; The signer must have been registered already - (asserts! (is-some (get-signer-info signer)) ERR_SIGNER_NOT_FOUND) + + ;; The signer must have been registered already, and its signer key + ;; grant must still be active. + (try! (verify-signer-key-grant signer + (unwrap! (get-signer-info signer) ERR_SIGNER_NOT_FOUND) + )) ;; lock period must be in acceptable range. (asserts! (check-pox-lock-period num-cycles) ERR_INVALID_NUM_CYCLES) @@ -883,18 +1111,6 @@ ERR_INSUFFICIENT_STX ) - ;; Call `old-signer-manager`, and allow them to snapshot current - ;; data before updating. Do not throw any errors. - (match (contract-call? old-signer-manager checkpoint-staker tx-sender - first-reward-cycle (- prev-unlock-cycle current-cycle u1) false - ) - ;; Allow any errors - ok-val - ok-val - err-val - true - ) - ;; Remove the staker from all existing cycles (try! (remove-staker-from-cycles tx-sender (+ u1 current-cycle) (- prev-unlock-cycle current-cycle u1) true @@ -911,18 +1127,45 @@ signer: signer, }) - (ok { - unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), - staker: tx-sender, - signer: signer, - prev-unlock-height: prev-unlock-cycle, - unlock-cycle: unlock-cycle, - num-cycles: num-cycles, - amount-ustx: new-lock-amount, - }) + (let ((result { + unlock-burn-height: (reward-cycle-to-burn-height unlock-cycle), + staker: tx-sender, + signer: signer, + old-signer: old-signer, + prev-unlock-height: prev-unlock-cycle, + unlock-cycle: unlock-cycle, + num-cycles: num-cycles, + amount-ustx: new-lock-amount, + amount-increase: amount-increase, + cycles-to-extend: cycles-to-extend, + })) + (print (merge { topic: "stake-update" } result)) + (ok result) + ) ) ) +;; Announce that the staker has exited their L1 lockup. +;; +;; This contract call notifies PoX-5 to stop counting the staker for this +;; bond period. It zeroes the staker's shares, debits the share totals by +;; that amount, settles outstanding rewards, and flips +;; `has-announced-l1-early-exit` to true for the caller's active bond. +;; +;; The staker's locked STX is intentionally untouched and remains locked +;; through the bond period's normal unlock cycle. Only the staker's BTC +;; shares are wound down here. +;; +;; Only the staker who is currently registered for a bond can successfully +;; call this function; other contracts cannot forward this call. +;; +;; Preconditions, for successfully calling this function are: +;; 1. The caller is the staker. +;; 2. The staker is an L1 bondholder, not an sBTC bondholders. sBTC +;; bondholders must use `unstake-sbtc` instead. +;; 3. The `old-signer-manager` matches the staker's signer. +;; 4. The staker has not already called this function for their active +;; bond. (define-public (announce-l1-early-exit (staker principal) (old-signer-manager ) @@ -932,53 +1175,58 @@ (membership (unwrap! (get-bond-membership staker) ERR_NOT_BOND_PARTICIPANT)) (bond-index (get bond-index membership)) (signer (get signer membership)) - (bond (unwrap-panic (get-protocol-bond bond-index))) - (amount-sats (get-staker-shares-staked-for-cycle staker true bond-index signer)) - (current-total-shares (get-total-shares-staked-for-cycle true bond-index)) - (current-shares (get-signer-shares-staked-for-cycle signer true bond-index)) + (current-cycle (current-pox-reward-cycle)) + (bond-start-cycle (bond-period-to-reward-cycle bond-index)) + (bond-end-cycle (bond-period-to-reward-cycle (+ bond-index u6))) + (current-total-staked (get-total-sbtc-staked-for-bond bond-index)) + (first-changed-reward-cycle (clamp current-cycle bond-start-cycle bond-end-cycle)) + (amount-sats (get amount-sats membership)) ) - ;; Only the early unlock admin for this bond period can call this function. + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + + ;; Only the staker themselves can announce their L1 early exit. ;; Calling via other contracts is not allowed. (asserts! - (and (is-eq contract-caller tx-sender) (is-eq contract-caller (get early-unlock-admin bond))) + (and (is-eq contract-caller tx-sender) (is-eq contract-caller staker)) ERR_UNAUTHORIZED ) (asserts! (get is-l1-lock membership) ERR_CANNOT_ANNOUNCE_L1_EARLY_UNLOCK) (asserts! (is-eq old-signer signer) ERR_INVALID_OLD_SIGNER_MANAGER) - - ;; Call `old-signer-manager`, and allow them to snapshot current - ;; data before updating. Do not throw any errors. - (match (contract-call? old-signer-manager checkpoint-staker staker bond-index u1 - true - ) - ok-val ok-val - err-val true + (asserts! (not (has-announced-l1-early-exit bond-index staker)) + ERR_L1_EARLY_EXIT_ALREADY_ANNOUNCED ) - (settle-rewards signer true bond-index) + ;; Settle rewards before updating state + (settle-rewards signer current-cycle (some bond-index)) + (settle-staker-rewards signer current-cycle (some bond-index) staker) - (map-set staker-shares-staked-for-cycle { - is-bond: true, - staker: staker, - signer: signer, - index: bond-index, - } - u0 + (try! (remove-staker-from-bond-cycles staker signer bond-index + first-changed-reward-cycle + (- bond-end-cycle first-changed-reward-cycle) amount-sats + )) + + (map-set protocol-bond-memberships staker + (merge membership { amount-sats: u0 }) ) - (map-set signer-shares-staked-for-cycle { - is-bond: true, - signer: signer, - index: bond-index, - } - (- current-shares amount-sats) + (map-set protocol-bonds-total-staked bond-index + (- current-total-staked amount-sats) ) - (map-set total-shares-staked-for-cycle { - index: bond-index, - is-bond: true, + (map-set protocol-bond-l1-early-exit-announced { + bond-index: bond-index, + staker: staker, } - (- current-total-shares amount-sats) + true + ) + (let ((result { + staker: staker, + signer: signer, + bond-index: bond-index, + amount-sats-released: amount-sats, + })) + (print (merge { topic: "announce-l1-early-exit" } result)) + (ok result) ) - (ok true) ) ) @@ -995,13 +1243,15 @@ )) (bond-index (get bond-index membership)) (signer (get signer membership)) - (current-amount-sats (get-staker-shares-staked-for-cycle staker true bond-index signer)) - (current-total-shares (get-total-shares-staked-for-cycle true bond-index)) - (current-shares (get-signer-shares-staked-for-cycle signer true bond-index)) + (current-cycle (current-pox-reward-cycle)) + (bond-start-cycle (bond-period-to-reward-cycle bond-index)) + (bond-end-cycle (bond-period-to-reward-cycle (+ bond-index u6))) + (first-changed-reward-cycle (clamp current-cycle bond-start-cycle bond-end-cycle)) + (num-cycles (- bond-end-cycle first-changed-reward-cycle)) + (current-amount-sats (get amount-sats membership)) (current-total-sbtc-staked (get-total-sbtc-staked)) ;; Cannot withdrawal more than they've staked (new-amount-sats (try! (if (<= amount-to-withdrawal-sats current-amount-sats) - (ok (- current-amount-sats amount-to-withdrawal-sats)) ERR_INVALID_UNSTAKE_SBTC_AMOUNT ))) @@ -1017,39 +1267,31 @@ ;; must be called directly by the tx-sender or by an allowed contract-caller (try! (check-caller-allowed)) - ;; Call `signer-manager`, and allow them to snapshot current - ;; data before updating. Do not throw any errors. - (match (contract-call? signer-manager checkpoint-staker staker bond-index u1 - true - ) - ok-val ok-val - err-val true - ) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) - ;; Take a snapshot of the signer's current rewards - (settle-rewards signer true bond-index) + ;; Take a snapshot of the staker's and signer's current rewards + (settle-rewards signer current-cycle (some bond-index)) + (settle-staker-rewards signer current-cycle (some bond-index) tx-sender) - (map-set staker-shares-staked-for-cycle { - is-bond: true, - staker: staker, - signer: signer, - index: bond-index, - } - new-amount-sats - ) - (map-set signer-shares-staked-for-cycle { - is-bond: true, - signer: signer, - index: bond-index, - } - (- current-shares amount-to-withdrawal-sats) - ) - (map-set total-shares-staked-for-cycle { - is-bond: true, - index: bond-index, - } - (- current-total-shares amount-to-withdrawal-sats) + ;; We need to update each affected cycle with this staker's new amount. Instead of + ;; mutating each cycle, we instead re-use existing "remove" and "add" helpers. + (try! (remove-staker-from-bond-cycles staker signer bond-index + first-changed-reward-cycle num-cycles current-amount-sats + )) + (try! (add-staker-to-bond-cycles staker signer bond-index + first-changed-reward-cycle num-cycles new-amount-sats + )) + + (map-set protocol-bond-memberships staker + (merge membership { amount-sats: new-amount-sats }) ) + (map-set protocol-bonds-total-staked bond-index + (- (get-total-sbtc-staked-for-bond bond-index) + amount-to-withdrawal-sats + )) + + ;; Mutate the total sBTC staked (var-set total-sbtc-staked (- current-total-sbtc-staked amount-to-withdrawal-sats) ) @@ -1063,11 +1305,16 @@ )) )) - (ok { - staker: staker, - signer: signer, - new-amount-sats: new-amount-sats, - }) + (let ((result { + staker: staker, + signer: signer, + bond-index: bond-index, + amount-withdrawn-sats: amount-to-withdrawal-sats, + new-amount-sats: new-amount-sats, + })) + (print (merge { topic: "unstake-sbtc" } result)) + (ok result) + ) ) ) @@ -1093,15 +1340,8 @@ ERR_UNSTAKE_IN_PREPARE_PHASE ) - ;; Call `old-signer-manager`, and allow them to snapshot current - ;; data before updating. Do not throw any errors. - (match (contract-call? old-signer-manager checkpoint-staker tx-sender - (+ current-cycle u1) (- prev-unlock-cycle current-cycle u1) - false - ) - ok-val ok-val - err-val true - ) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) ;; Remove the staker from all existing cycles (try! (remove-staker-from-cycles tx-sender (+ u1 current-cycle) @@ -1115,13 +1355,17 @@ signer: old-signer, }) - (ok { - staker: tx-sender, - amount-ustx: (get amount-ustx current-info), - first-reward-cycle: first-reward-cycle, - unlock-cycle: unlock-cycle, - unlock-burn-height: (reward-cycle-to-unlock-height unlock-cycle), - }) + (let ((result { + staker: tx-sender, + signer: old-signer, + amount-ustx: (get amount-ustx current-info), + first-reward-cycle: first-reward-cycle, + unlock-cycle: unlock-cycle, + unlock-burn-height: (reward-cycle-to-burn-height unlock-cycle), + })) + (print (merge { topic: "unstake" } result)) + (ok result) + ) ) ) @@ -1170,22 +1414,22 @@ (let ( (accumulator (try! accumulator-res)) (staker (get staker accumulator)) - (cycle (+ cycle-index (get first-reward-cycle accumulator))) + (reward-cycle (+ cycle-index (get first-reward-cycle accumulator))) (membership (unwrap! (map-get? staker-signer-cycle-memberships { staker: staker, - cycle: cycle, + cycle: reward-cycle, }) ERR_NOT_STAKING )) (signer (get signer membership)) ;; Get the total uSTX delegated (through protocol bonds and STX-only ;; staking) to this signer. - (cur-delegated-for-signer (get-amount-delegated-for-signer signer cycle)) + (cur-delegated-for-signer (get-amount-delegated-for-signer signer reward-cycle)) ;; uSTX staked for this signer (through STX-only staking) - (cur-staked-for-signer (get-signer-shares-staked-for-cycle signer false cycle)) + (cur-staked-for-signer (get-signer-shares-staked-for-cycle signer reward-cycle none)) ;; Total uSTX staked (through stx-only staking) this cycle - (total-shares-staked (get-total-shares-staked-for-cycle false cycle)) + (total-shares-staked (get-total-shares-staked-for-cycle reward-cycle none)) (amount (get amount-ustx membership)) (is-stx-staking (get is-stx-staking accumulator)) (stake-amount (if is-stx-staking @@ -1193,26 +1437,28 @@ u0 )) (new-delegated (- cur-delegated-for-signer amount)) - (is-in-signer-set (is-some (get-signer-set-item-for-cycle signer cycle))) + (is-in-signer-set (is-some (get-signer-set-item-for-cycle signer reward-cycle))) ) - ;; Crystallize STX-only rewards before mutating anything - (settle-rewards signer false cycle) + ;; Settle STX-only rewards before mutating anything + (settle-rewards signer reward-cycle none) + (settle-staker-rewards signer reward-cycle none staker) + (if is-in-signer-set (if (< new-delegated SIGNER_SET_MIN_USTX) ;; They've crossed back below the threshold - remove from the signer set ;; and remove from reward calculations. (begin - (try! (remove-staker-from-set-for-cycle signer cycle)) + (try! (remove-staker-from-set-for-cycle signer reward-cycle)) (map-set signer-shares-staked-for-cycle { - index: cycle, + reward-cycle: reward-cycle, signer: signer, - is-bond: false, + bond-index: none, } u0 ) (map-set total-shares-staked-for-cycle { - index: cycle, - is-bond: false, + reward-cycle: reward-cycle, + bond-index: none, } (- total-shares-staked cur-staked-for-signer) ) @@ -1220,14 +1466,14 @@ ;; They are in the signer set - update reward calculations (begin (map-set total-shares-staked-for-cycle { - index: cycle, - is-bond: false, + reward-cycle: reward-cycle, + bond-index: none, } (- total-shares-staked stake-amount) ) (map-set signer-shares-staked-for-cycle { - index: cycle, - is-bond: false, + reward-cycle: reward-cycle, + bond-index: none, signer: signer, } (- cur-staked-for-signer stake-amount) @@ -1239,33 +1485,33 @@ ;; Remove this staker from this signer (map-delete staker-signer-cycle-memberships { staker: staker, - cycle: cycle, + cycle: reward-cycle, }) ;; Update amount delegated (map-set signer-delegated-per-cycle { - cycle: cycle, + cycle: reward-cycle, signer: signer, } new-delegated ) ;; Remove amount for staker (map-delete staker-shares-staked-for-cycle { - index: cycle, - is-bond: false, + reward-cycle: reward-cycle, + bond-index: none, staker: staker, signer: signer, }) ;; Update amount staked (map-set signer-pending-staked-ustx-per-cycle { signer: signer, - cycle: cycle, + cycle: reward-cycle, } - (- (get-signer-pending-staked-ustx-per-cycle signer cycle) + (- (get-signer-pending-staked-ustx-per-cycle signer reward-cycle) stake-amount )) ;; Update total amount delegated this cycle - (map-set ustx-delegated-per-cycle cycle - (- (get-ustx-delegated-for-cycle cycle) amount) + (map-set ustx-delegated-per-cycle reward-cycle + (- (get-ustx-delegated-for-cycle reward-cycle) amount) ) (ok accumulator) ) @@ -1340,16 +1586,18 @@ )) (staker (get staker accumulator)) (prev-staked (get-signer-pending-staked-ustx-per-cycle signer cycle)) - (prev-total-shares-staked (get-total-shares-staked-for-cycle false cycle)) + (prev-total-shares-staked (get-total-shares-staked-for-cycle cycle none)) (new-delegated (+ cur-delegated-for-signer amount)) ) ;; Crystallize STX-only rewards before mutating anything - (settle-rewards signer false cycle) + (settle-rewards signer cycle none) + (settle-staker-rewards signer cycle none staker) + (if (>= new-delegated SIGNER_SET_MIN_USTX) (begin (map-set signer-shares-staked-for-cycle { - index: cycle, - is-bond: false, + reward-cycle: cycle, + bond-index: none, signer: signer, } (+ prev-staked stake-amount) @@ -1357,18 +1605,18 @@ (if (< cur-delegated-for-signer SIGNER_SET_MIN_USTX) ;; They just crossed the threshold - add to signer set and add to reward calculations (begin - (try! (add-signer-to-set-for-cycle signer cycle)) + (add-signer-to-set-for-cycle signer cycle) (map-set total-shares-staked-for-cycle { - index: cycle, - is-bond: false, + reward-cycle: cycle, + bond-index: none, } (+ prev-total-shares-staked prev-staked stake-amount) ) ) ;; They're already over the threshold - update the total by just `stake-amount` (map-set total-shares-staked-for-cycle { - index: cycle, - is-bond: false, + reward-cycle: cycle, + bond-index: none, } (+ prev-total-shares-staked stake-amount) ) @@ -1403,8 +1651,8 @@ ;; Update the amount staked for this staker (map-set staker-shares-staked-for-cycle { staker: staker, - index: cycle, - is-bond: false, + reward-cycle: cycle, + bond-index: none, signer: signer, } stake-amount @@ -1413,17 +1661,211 @@ (map-set ustx-delegated-per-cycle cycle (+ (get-ustx-delegated-for-cycle cycle) amount) ) + ;; Mark settled rewards for this cycle + (map-set staker-rewards-per-token-settled-for-cycle { + reward-cycle: cycle, + bond-index: none, + signer: signer, + staker: staker, + } + (get-signer-rewards-per-token-for-cycle signer cycle none) + ) (ok accumulator) ) ) -(define-private (lock-sbtc (amount uint)) - (begin - (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token - transfer amount tx-sender current-contract none +(define-private (add-staker-to-bond-cycles + (staker principal) + (signer principal) + (bond-index uint) + (first-reward-cycle uint) + (num-cycles uint) + (amount-sats uint) + ) + (ok (try! (fold add-staker-to-bond-for-cycle + (unwrap-panic (slice? (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11) u0 num-cycles)) + (ok { + staker: staker, + signer: signer, + amount-sats: amount-sats, + first-reward-cycle: first-reward-cycle, + bond-index: bond-index, + }) + ))) +) + +(define-private (add-staker-to-bond-for-cycle + (cycle-index uint) + (accumulator-res (response { + signer: principal, + staker: principal, + bond-index: uint, + amount-sats: uint, + first-reward-cycle: uint, + } + uint )) - (var-set total-sbtc-staked (+ (var-get total-sbtc-staked) amount)) - (ok amount) + ) + (let ( + (accumulator (try! accumulator-res)) + (reward-cycle (+ cycle-index (get first-reward-cycle accumulator))) + (signer (get signer accumulator)) + (bond-index (get bond-index accumulator)) + (amount-sats (get amount-sats accumulator)) + (current-total-staked (get-total-shares-staked-for-cycle reward-cycle (some bond-index))) + (current-signer-staked (get-signer-shares-staked-for-cycle signer reward-cycle + (some bond-index) + )) + ) + ;; Update total shares staked for this cycle + (map-set total-shares-staked-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some bond-index), + } + (+ current-total-staked amount-sats) + ) + ;; Update total shares for this signer + (map-set signer-shares-staked-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some bond-index), + signer: signer, + } + (+ current-signer-staked amount-sats) + ) + ;; Update staker's shares + (map-set staker-shares-staked-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some bond-index), + signer: signer, + staker: (get staker accumulator), + } + amount-sats + ) + ;; Mark settled rewards for this cycle + (map-set staker-rewards-per-token-settled-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some bond-index), + signer: signer, + staker: (get staker accumulator), + } + (get-signer-rewards-per-token-for-cycle signer reward-cycle + (some bond-index) + )) + (ok accumulator) + ) +) + +(define-private (remove-staker-from-bond-cycles + (staker principal) + (signer principal) + (bond-index uint) + (first-reward-cycle uint) + (num-cycles uint) + (amount-sats uint) + ) + (ok (try! (fold remove-staker-from-bond-for-cycle + (unwrap-panic (slice? (list u0 u1 u2 u3 u4 u5 u6 u7 u8 u9 u10 u11) u0 num-cycles)) + (ok { + staker: staker, + signer: signer, + amount-sats: amount-sats, + first-reward-cycle: first-reward-cycle, + bond-index: bond-index, + }) + ))) +) + +(define-private (remove-staker-from-bond-for-cycle + (cycle-index uint) + (accumulator-res (response { + signer: principal, + staker: principal, + bond-index: uint, + amount-sats: uint, + first-reward-cycle: uint, + } + uint + )) + ) + (let ( + (accumulator (try! accumulator-res)) + (reward-cycle (+ cycle-index (get first-reward-cycle accumulator))) + (signer (get signer accumulator)) + (bond-index (get bond-index accumulator)) + (amount-sats (get amount-sats accumulator)) + (current-total-staked (get-total-shares-staked-for-cycle reward-cycle (some bond-index))) + (current-signer-staked (get-signer-shares-staked-for-cycle signer reward-cycle + (some bond-index) + )) + ) + ;; Update total shares staked for this cycle + (map-set total-shares-staked-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some bond-index), + } + (- current-total-staked amount-sats) + ) + ;; Update total shares for this signer + (map-set signer-shares-staked-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some bond-index), + signer: signer, + } + (- current-signer-staked amount-sats) + ) + ;; Update staker's shares + (map-set staker-shares-staked-for-cycle { + reward-cycle: reward-cycle, + bond-index: (some bond-index), + signer: signer, + staker: (get staker accumulator), + } + u0 + ) + (ok accumulator) + ) +) + +;; Move a staker's custodied sBTC from `old-sbtc` to `new-sbtc`, transferring +;; only the net difference: pull the increase from the staker, or refund the +;; decrease. `total-sbtc-staked` is updated by the net change. A registration +;; with no rollover passes `old-sbtc` of `u0`, which transfers the full amount. +;; A no-op when the two are equal. +(define-private (roll-sbtc + (staker principal) + (old-sbtc uint) + (new-sbtc uint) + ) + (begin + (if (> new-sbtc old-sbtc) + (let ((delta (- new-sbtc old-sbtc))) + (try! (contract-call? + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer delta tx-sender current-contract none + )) + (var-set total-sbtc-staked (+ (var-get total-sbtc-staked) delta)) + ) + (if (< new-sbtc old-sbtc) + (let ((delta (- old-sbtc new-sbtc))) + (try! (as-contract? + ((with-ft + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" delta + )) + (try! (contract-call? + 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer delta tx-sender staker none + )) + )) + (var-set total-sbtc-staked + (- (var-get total-sbtc-staked) delta) + ) + ) + ;; new-sbtc == old-sbtc, no transfer needed + true + ) + ) + (ok true) ) ) @@ -1446,19 +1888,21 @@ amount: uint, } ), - unlock-bytes: (buff 683), + staker-unlock-bytes: (buff 683), }) ) (let ( (bond (unwrap! (get-protocol-bond bond-index) ERR_BOND_NOT_FOUND)) (expected-timelock-output (construct-lockup-output-script staker (get-bond-l1-unlock-height bond-index) - (get unlock-bytes lockups) (get early-unlock-bytes bond) + (get staker-unlock-bytes lockups) + (get early-unlock-bytes bond) )) (accumulation (try! (fold validate-l1-lockup (get outputs lockups) (ok { sum: u0, expected-script-hash: expected-timelock-output, + seen-outpoints: (list), }) ))) ) @@ -1467,6 +1911,12 @@ ) ;; Fold function for validating l1 lockup info +;; +;; - `expected-script-hash` is the timelock script that the lockup must match +;; - `sum` is the running total of sats from all valid lockups processed so far. +;; - `seen-outpoints` tracks every (txid, output-index) pair already credited +;; in this call. Duplicate entries is rejected via +;; ERR_DUPLICATE_LOCKUP_OUTPOINT. (define-private (validate-l1-lockup (lockup { height: uint, @@ -1481,6 +1931,10 @@ (accumulator-res (response { expected-script-hash: (buff 34), sum: uint, + seen-outpoints: (list 10 { + txid: (buff 32), + output-index: uint, + }), } uint )) @@ -1492,6 +1946,11 @@ (output (try! (get-bitcoin-tx-output? (get tx lockup) (get output-index lockup)))) (reversed-txid (get txid output)) (txid (reverse-buff32 reversed-txid)) + (outpoint { + txid: txid, + output-index: (get output-index lockup), + }) + (seen-outpoints (get seen-outpoints accumulator)) ) (asserts! (verify-block-header (get header lockup) (get height lockup)) ERR_INVALID_BTC_HEADER @@ -1502,6 +1961,9 @@ (asserts! (is-eq (get amount output) (get amount lockup)) ERR_INVALID_LOCKUP_AMOUNT ) + (asserts! (is-none (index-of? seen-outpoints outpoint)) + ERR_DUPLICATE_LOCKUP_OUTPOINT + ) ;; verify merkle proof (asserts! (or @@ -1517,10 +1979,27 @@ (ok { expected-script-hash: (get expected-script-hash accumulator), sum: (+ (get sum accumulator) (get amount output)), + seen-outpoints: (unwrap-panic (as-max-len? (append seen-outpoints outpoint) u10)), }) ) ) +(define-private (get-l1-lockup-summary (lockup { + height: uint, + tx: (buff 100000), + output-index: uint, + header: (buff 80), + leaf-hashes: (list 14 (buff 32)), + tx-count: uint, + tx-index: uint, + amount: uint, +})) + { + txid: (get-reversed-txid (get tx lockup)), + output-index: (get output-index lockup), + } +) + ;;; Reward calculation ;; Returns the total balance of rewards received by the contract @@ -1554,8 +2033,12 @@ u1 )) (cur-reserve (var-get reserve-balance)) - (accrued-rewards (get-new-rewards)) + (gross-accrued-rewards (get-new-rewards)) + (stx-cycle (burn-height-to-reward-cycle calculation-height)) ) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + ;; verify that we are able to compute here (asserts! (> calculation-height last-calc) ERR_DISTRIBUTION_ALREADY_COMPUTED @@ -1568,57 +2051,61 @@ (bond-distributions (try! (fold calculate-bond-rewards bond-periods (ok { last-bond-stx-value-ratio: none, - available-rewards: accrued-rewards, + available-rewards: gross-accrued-rewards, last-bond-index: none, calculation-height: calculation-height, + reward-cycle: stx-cycle, }) ))) (remaining-rewards (get available-rewards bond-distributions)) - (new-reserve (/ (* remaining-rewards RESERVE_RATIO) u10000)) - (stx-staker-rewards (- remaining-rewards new-reserve)) - (stx-cycle (burn-height-to-reward-cycle calculation-height)) - (cycle-staked-ustx (get-total-shares-staked-for-cycle false stx-cycle)) - (current-rewards-per-ustx (get-rewards-per-token-for-cycle false stx-cycle)) + (reserve-cut (/ (* remaining-rewards RESERVE_RATIO) u10000)) + (stx-staker-rewards (- remaining-rewards reserve-cut)) + (cycle-staked-ustx (get-total-shares-staked-for-cycle stx-cycle none)) + (current-rewards-per-ustx (get-rewards-per-token-for-cycle stx-cycle none)) (prev-accounted-rewards (var-get last-accounted-rewards-only)) ;; If no STX is staked this cycle, the staker cut will be applied to the reserve. (no-stx-stakers (is-eq cycle-staked-ustx u0)) - (new-rewards-per-ustx (if no-stx-stakers + (accrued-rewards-per-ustx (if no-stx-stakers u0 (/ (* stx-staker-rewards PRECISION) cycle-staked-ustx) )) - (next-rewards-per-ustx (+ current-rewards-per-ustx new-rewards-per-ustx)) + (cumulative-rewards-per-ustx (+ current-rewards-per-ustx accrued-rewards-per-ustx)) ;; When no STX is staked, fold the staker cut into the reserve, otherwise zero. - (stranded-staker-cut (if no-stx-stakers + (unallocated-staker-cut (if no-stx-stakers stx-staker-rewards u0 )) + (reserve-deposit (+ reserve-cut unallocated-staker-cut)) + (new-reserve-balance (+ cur-reserve reserve-deposit)) ) - (print { - topic: "calculate-rewards", - bond-periods: bond-periods, - calculation-height: calculation-height, - remaining-rewards: remaining-rewards, - accrued-rewards: accrued-rewards, - stx-staker-rewards: stx-staker-rewards, - stx-cycle: stx-cycle, - cycle-staked-ustx: cycle-staked-ustx, - next-rewards-per-ustx: next-rewards-per-ustx, - stranded-staker-cut: stranded-staker-cut, - }) - (var-set reserve-balance - (+ cur-reserve new-reserve stranded-staker-cut) - ) + (var-set reserve-balance new-reserve-balance) (var-set last-reward-compute-height calculation-height) (var-set last-accounted-rewards-only - (+ prev-accounted-rewards (- accrued-rewards new-reserve)) - ) + (+ prev-accounted-rewards + (- gross-accrued-rewards reserve-deposit) + )) (map-set rewards-per-token-for-cycle { - index: stx-cycle, - is-bond: false, + reward-cycle: stx-cycle, + bond-index: none, } - next-rewards-per-ustx + cumulative-rewards-per-ustx + ) + (let ((result { + bond-periods: bond-periods, + calculation-height: calculation-height, + gross-accrued-rewards: gross-accrued-rewards, + total-bond-rewards: (- gross-accrued-rewards remaining-rewards), + reserve-deposit: reserve-deposit, + reserve-balance: new-reserve-balance, + stx-cycle: stx-cycle, + total-stx-staker-rewards: stx-staker-rewards, + cycle-staked-ustx: cycle-staked-ustx, + accrued-rewards-per-ustx: accrued-rewards-per-ustx, + cumulative-rewards-per-ustx: cumulative-rewards-per-ustx, + })) + (print (merge { topic: "calculate-rewards" } result)) + (ok result) ) - (ok true) ) ) ) @@ -1634,6 +2121,7 @@ ;; How much rewards are available to be distributed available-rewards: uint, calculation-height: uint, + reward-cycle: uint, } uint )) @@ -1641,7 +2129,8 @@ (let ( (accumulator (try! accumulator-res)) (bond (unwrap! (map-get? protocol-bonds bond-index) ERR_BOND_NOT_FOUND)) - (total-sats (get-total-shares-staked-for-cycle true bond-index)) + (reward-cycle (get reward-cycle accumulator)) + (total-sats (get-total-shares-staked-for-cycle reward-cycle (some bond-index))) (available-rewards (get available-rewards accumulator)) ;; How much sBTC the bond is supposed to earn per calculation, ;; which is (totalSats * apy) / 50 @@ -1653,9 +2142,9 @@ available-rewards )) (stx-value-ratio (get stx-value-ratio bond)) - (current-rewards-per-token (get-rewards-per-token-for-cycle true bond-index)) + (current-rewards-per-token (get-rewards-per-token-for-cycle reward-cycle (some bond-index))) ;; Prevent divide-by-zero - (new-rewards-per-token (if (is-eq total-sats u0) + (accrued-rewards-per-sat (if (is-eq total-sats u0) u0 (/ (* earned PRECISION) total-sats) )) @@ -1684,10 +2173,10 @@ ) (map-set rewards-per-token-for-cycle { - is-bond: true, - index: bond-index, + reward-cycle: reward-cycle, + bond-index: (some bond-index), } - (+ current-rewards-per-token new-rewards-per-token) + (+ current-rewards-per-token accrued-rewards-per-sat) ) (asserts! @@ -1702,7 +2191,10 @@ topic: "bond-distribution", bond-index: bond-index, target-yield: target-yield, - earned: earned, + bond-rewards: earned, + bond-staked-sats: total-sats, + accrued-rewards-per-sat: accrued-rewards-per-sat, + cumulative-rewards-per-sat: (+ current-rewards-per-token accrued-rewards-per-sat), }) (ok { @@ -1710,28 +2202,57 @@ last-bond-index: (some bond-index), available-rewards: (- available-rewards earned), calculation-height: calculation-height, + reward-cycle: reward-cycle, }) ) ) ;; Get the total amount of rewards earned since the last ;; rewards snapshot. -;; -;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` (define-read-only (get-earned (signer principal) - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) - (let ( - (shares (get-signer-shares-staked-for-cycle signer is-bond index)) - (rpt-current (get-rewards-per-token-for-cycle is-bond index)) - (rpt-paid (get-signer-rewards-per-token-settled-for-cycle signer is-bond index)) - (pending (get-signer-unclaimed-rewards-for-cycle signer is-bond index)) - (newly-earned (/ (* shares (- rpt-current rpt-paid)) PRECISION)) + (compute-earned-rewards + (get-signer-shares-staked-for-cycle signer reward-cycle bond-index) + (get-rewards-per-token-for-cycle reward-cycle bond-index) + (get-signer-rewards-per-token-settled-for-cycle signer reward-cycle + bond-index + ) + (get-signer-unclaimed-rewards-for-cycle signer reward-cycle bond-index) + ) +) + +;; Get the total amount of _staker_ rewards earned since the last +;; rewards snapshot. +(define-read-only (get-earned-staker-rewards + (signer principal) + (reward-cycle uint) + (bond-index (optional uint)) + (staker principal) + ) + (compute-earned-rewards + (get-staker-shares-staked-for-cycle staker reward-cycle bond-index signer) + (get-signer-rewards-per-token-for-cycle signer reward-cycle bond-index) + (get-staker-rewards-per-token-settled-for-cycle signer reward-cycle + bond-index staker ) - (+ pending newly-earned) + (get-staker-unclaimed-rewards-for-cycle signer reward-cycle bond-index + staker + )) +) + +;; Pure math formula for computing rewards earned since the last snapshot +;; +;; `earned = (shares * (rpt - rptPaid)) / PRECISION + pending` +(define-read-only (compute-earned-rewards + (shares uint) + (rpt-current uint) + (rpt-paid uint) + (pending uint) ) + (+ pending (/ (* shares (- rpt-current rpt-paid)) PRECISION)) ) (define-public (claim-rewards @@ -1740,16 +2261,20 @@ ) (let ( (signer contract-caller) - (stx-rewards (update-claimable-rewards signer false reward-cycle)) + (stx-rewards (update-claimable-rewards signer reward-cycle none)) (bond-rewards (fold update-claimable-bond-rewards bond-periods { signer: signer, total: u0, bond-rewards: (list), + reward-cycle: reward-cycle, })) (bond-totals (get total bond-rewards)) (total-rewards (+ (get earned stx-rewards) bond-totals)) (prev-accrued-rewards (var-get last-accounted-rewards-only)) ) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + (asserts! (> total-rewards u0) ERR_NO_CLAIMABLE_REWARDS) (try! (as-contract? ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token @@ -1764,19 +2289,53 @@ (- prev-accrued-rewards total-rewards) ) + (let ((result { + stx-rewards: stx-rewards, + bond-rewards: (get bond-rewards bond-rewards), + bond-totals: bond-totals, + total-rewards: total-rewards, + })) + (print (merge { + topic: "claim-rewards", + reward-cycle: reward-cycle, + signer-manager: contract-caller, + } + result + )) + (ok result) + ) + ) +) + +;; As a signer manager contract, mark a specific staker as having claimed +;; rewards. This is used to mutate internal rewards settlement state. +;; +;; This is only callable by the signer manager contract. +(define-public (claim-staker-rewards-for-signer + (staker principal) + (reward-cycle uint) + (bond-index (optional uint)) + ) + (let ((rewards-info (settle-staker-rewards contract-caller reward-cycle bond-index staker))) + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + (map-set staker-unclaimed-rewards-for-cycle { + reward-cycle: reward-cycle, + bond-index: bond-index, + signer: contract-caller, + staker: staker, + } + u0 + ) (print { - topic: "claim-rewards", - stx-rewards: stx-rewards, - bond-rewards: (get bond-rewards bond-rewards), - bond-totals: bond-totals, - total-rewards: total-rewards, - }) - (ok { - stx-rewards: stx-rewards, - bond-rewards: (get bond-rewards bond-rewards), - bond-totals: bond-totals, - total-rewards: total-rewards, + topic: "claim-staker-rewards-for-signer", + signer-manager: contract-caller, + staker: staker, + reward-cycle: reward-cycle, + bond-index: bond-index, + rewards-claimed: (get earned rewards-info), }) + (ok rewards-info) ) ) @@ -1786,15 +2345,15 @@ ;; Returns the newly claimable amount. Does NOT transfer funds out. (define-private (update-claimable-rewards (signer principal) - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) - (let ((earned (settle-rewards signer is-bond index))) + (let ((earned (settle-rewards signer reward-cycle bond-index))) ;; After crystallization, all earnings live in pending. ;; Zero out pending since we're about to pay it. (map-set signer-unclaimed-rewards-for-cycle { - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, signer: signer, } u0 @@ -1808,6 +2367,7 @@ (accumulator { signer: principal, total: uint, + reward-cycle: uint, bond-rewards: (list 6 { earned: uint, @@ -1817,7 +2377,9 @@ ), }) ) - (let ((rewards-info (update-claimable-rewards (get signer accumulator) true bond-index))) + (let ((rewards-info (update-claimable-rewards (get signer accumulator) + (get reward-cycle accumulator) (some bond-index) + ))) { signer: (get signer accumulator), total: (+ (get total accumulator) (get earned rewards-info)), @@ -1825,35 +2387,91 @@ (unwrap-panic (as-max-len? (get bond-rewards accumulator) u5)) (list (merge rewards-info { bond-index: bond-index })) ), + reward-cycle: (get reward-cycle accumulator), } ) ) ;; Update all earned-but-unclaimed rewards for a signer, and update the snapshot -;; (signer-rewards-per-token-paid) for the signer. +;; (signer-rewards-per-token-settled-for-cycle) for the signer. ;; ;; This MUST be called before any update to `signer-shares-staked-for-cycle`, ;; because changes to that state will effect rewards calculations. (define-private (settle-rewards (signer principal) - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) (let ( - (earned (get-earned signer is-bond index)) - (rewards-per-token (get-rewards-per-token-for-cycle is-bond index)) + (earned (get-earned signer reward-cycle bond-index)) + (rewards-per-token (get-rewards-per-token-for-cycle reward-cycle bond-index)) ) (map-set signer-unclaimed-rewards-for-cycle { - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, signer: signer, } earned ) (map-set signer-rewards-per-token-settled-for-cycle { - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, + signer: signer, + } + rewards-per-token + ) + (if (> + (get-signer-shares-staked-for-cycle signer reward-cycle + bond-index + ) + u0 + ) + (map-set signer-rewards-per-token-for-cycle { + signer: signer, + reward-cycle: reward-cycle, + bond-index: bond-index, + } + rewards-per-token + ) + true + ) + { + earned: earned, + rewards-per-token: rewards-per-token, + } + ) +) + +;; Update all earned-but-unclaimed rewards for a staker, and update the snapshot +;; (staker-rewards-per-token-settled-for-cycle) for the staker. +;; +;; This MUST be called before any update to `staker-shares-staked-for-cycle`, +;; because changes to that state will effect rewards calculations. +(define-private (settle-staker-rewards + (signer principal) + (reward-cycle uint) + (bond-index (optional uint)) + (staker principal) + ) + (let ( + (earned (get-earned-staker-rewards signer reward-cycle bond-index staker)) + (rewards-per-token (get-signer-rewards-per-token-for-cycle signer reward-cycle + bond-index + )) + ) + (map-set staker-unclaimed-rewards-for-cycle { + reward-cycle: reward-cycle, + bond-index: bond-index, + signer: signer, + staker: staker, + } + earned + ) + (map-set staker-rewards-per-token-settled-for-cycle { + reward-cycle: reward-cycle, + bond-index: bond-index, signer: signer, + staker: staker, } rewards-per-token ) @@ -1940,8 +2558,28 @@ } ) -;; TODO: private fn to transfer funds from reserve -;; (define-private (transfer-from-reserve (amount uint) (recipient uint))) +;; Transfer funds from reserve. This is private and not called anywhere in the +;; contract, so it can only be called by the node as part of consensus (via the +;; SIP process). +;; #[allow(unused_private_fn)] +(define-private (transfer-from-reserve + (amount uint) + (recipient principal) + ) + (let ((cur-reserve (var-get reserve-balance))) + (asserts! (>= cur-reserve amount) ERR_INSUFFICIENT_RESERVE_BALANCE) + (var-set reserve-balance (- cur-reserve amount)) + (try! (as-contract? + ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + "sbtc-token" amount + )) + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount current-contract recipient none + )) + )) + (ok true) + ) +) ;;; Signer key authorization functions @@ -1952,6 +2590,13 @@ (signer-sig (buff 65)) ) (begin + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + + ;; Only the signer contract itself can call this function to grant a signer key + (asserts! (is-eq contract-caller signer-manager) + ERR_UNAUTHORIZED_SIGNER_REGISTRATION + ) (asserts! (is-none (map-get? used-signer-key-grants { signer-key: signer-key, @@ -1993,20 +2638,41 @@ true ) - (ok true) + (print { + topic: "grant-signer-key", + signer-key: signer-key, + signer-manager: signer-manager, + auth-id: auth-id, + }) + + (ok { + signer-key: signer-key, + signer-manager: signer-manager, + auth-id: auth-id, + }) ) ) ;; Revoke a signer key grant for a staker. Only the Stacks principal ;; associated with `signer-key` can call this function. ;; +;; Revoking has two effects: it prevents future `register-signer` calls for +;; this (signer-key, signer-manager) pair, and, because every new-stake +;; entry point re-checks the grant via `verify-signer-key-grant`, it also +;; disables an already-registered manager from accepting any new stake. The +;; manager's `signers` entry is left intact so its outstanding obligations can +;; still be settled; those positions wind down as their bonds/stakes expire. +;; ;; Returns a boolean indicating whether the signer key grant existed. (define-public (revoke-signer-grant (signer-manager principal) (signer-key (buff 33)) ) (begin - ;; Validate that `tx-sender` has the same pubkey hash as `signer-key` + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + + ;; Validate that `contract-caller` has the same pubkey hash as `signer-key` (asserts! (is-eq (unwrap-panic (principal-construct? @@ -2016,14 +2682,23 @@ ) (hash160 signer-key) )) - tx-sender + contract-caller ) ERR_UNAUTHORIZED ) - (ok (map-delete signer-key-grants { + (print { + topic: "revoke-signer-grant", signer-key: signer-key, signer-manager: signer-manager, - })) + }) + (ok { + signer-key: signer-key, + signer-manager: signer-manager, + existed: (map-delete signer-key-grants { + signer-key: signer-key, + signer-manager: signer-manager, + }), + }) ) ) @@ -2084,14 +2759,6 @@ ) ) -;; Get the L1 unlock height for a given reward cycle. -;; This is equal to exactly halfway through the provided cycle. -(define-read-only (reward-cycle-to-unlock-height (cycle uint)) - (+ (reward-cycle-to-burn-height cycle) - (/ (var-get pox-reward-cycle-length) u2) - ) -) - ;; What's the current PoX reward cycle? (define-read-only (current-pox-reward-cycle) (burn-height-to-reward-cycle burn-block-height) @@ -2125,6 +2792,81 @@ )) ) +;; Reject calls that would modify the next reward cycle's signer / staker +;; set during the current cycle's prepare phase, when that set is frozen. +;; Used by `stake`, `stake-update`, `register-for-bond`, and +;; `update-bond-registration` as `(try! (verify-not-prepare-phase))`. +(define-private (verify-not-prepare-phase) + (ok (asserts! (not (is-in-prepare-phase (current-pox-reward-cycle))) + ERR_STAKE_IN_PREPARE_PHASE + )) +) + +;; The sBTC the staker currently has custodied in pox-5, derived from their +;; bond membership. Returns u0 when the staker has no bond membership, or +;; when their existing bond is an L1 lock (no sBTC is custodied for L1 +;; bonds). Used by `register-for-bond` and `stake` to compute the source +;; side of a `roll-sbtc` net transfer. +(define-read-only (get-staker-custodied-sbtc (staker principal)) + (match (map-get? protocol-bond-memberships staker) + m (if (get is-l1-lock m) + u0 + (get amount-sats m) + ) + u0 + ) +) + +;; True if `existing-membership` (when present) would overlap a new staking +;; term starting at `new-first-reward-cycle`. A bond whose term ends at or +;; before the new first cycle is non-overlapping. Callers wrap this in +;; their own `asserts!` so they can pick the appropriate error code +;; (`ERR_ALREADY_REGISTERED` in `register-for-bond`, `ERR_ALREADY_STAKED` +;; in `stake`). +(define-read-only (bond-overlaps-new-position? + (existing-membership (optional { + bond-index: uint, + amount-ustx: uint, + signer: principal, + is-l1-lock: bool, + amount-sats: uint, + })) + (new-first-reward-cycle uint) + ) + (match existing-membership + existing (> + (+ BOND_LENGTH_CYCLES + (bond-period-to-reward-cycle (get bond-index existing)) + ) + new-first-reward-cycle + ) + false + ) +) + +;; Reject a rollover attempt before the existing bond's L1 collateral would +;; have unlocked -- same window an L1 bond holder has to redirect their +;; BTC. No-op when there is no existing bond. Used by `register-for-bond` +;; and `stake` as `(try! (verify-bond-rollover-window existing-membership))`, +;; same shape as `verify-not-prepare-phase`. +(define-private (verify-bond-rollover-window (existing-membership (optional { + bond-index: uint, + amount-ustx: uint, + signer: principal, + is-l1-lock: bool, + amount-sats: uint, +}))) + (ok (asserts! + (match existing-membership + existing (>= burn-block-height + (get-bond-l1-unlock-height (get bond-index existing)) + ) + true + ) + ERR_ROLLOVER_TOO_EARLY + )) +) + (define-read-only (is-bond-active-at-height (bond-index uint) (calculation-height uint) @@ -2247,38 +2989,38 @@ ) (define-read-only (get-rewards-per-token-for-cycle - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) (default-to u0 (map-get? rewards-per-token-for-cycle { - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, }) ) ) (define-read-only (get-total-shares-staked-for-cycle - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) (default-to u0 (map-get? total-shares-staked-for-cycle { - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, }) ) ) (define-read-only (get-signer-shares-staked-for-cycle (signer principal) - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) (default-to u0 (map-get? signer-shares-staked-for-cycle { - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, signer: signer, }) ) @@ -2287,44 +3029,90 @@ ;; Get the amount of shares staked for a given staker in a certain cycle. (define-read-only (get-staker-shares-staked-for-cycle (staker principal) - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) (signer principal) ) (default-to u0 (map-get? staker-shares-staked-for-cycle { - index: index, staker: staker, - is-bond: is-bond, signer: signer, + reward-cycle: reward-cycle, + bond-index: bond-index, }) ) ) (define-read-only (get-signer-rewards-per-token-settled-for-cycle (signer principal) - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) (default-to u0 (map-get? signer-rewards-per-token-settled-for-cycle { signer: signer, - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, }) ) ) (define-read-only (get-signer-unclaimed-rewards-for-cycle (signer principal) - (is-bond bool) - (index uint) + (reward-cycle uint) + (bond-index (optional uint)) ) (default-to u0 (map-get? signer-unclaimed-rewards-for-cycle { signer: signer, - is-bond: is-bond, - index: index, + reward-cycle: reward-cycle, + bond-index: bond-index, + }) + ) +) + +(define-read-only (get-staker-rewards-per-token-settled-for-cycle + (signer principal) + (reward-cycle uint) + (bond-index (optional uint)) + (staker principal) + ) + (default-to u0 + (map-get? staker-rewards-per-token-settled-for-cycle { + staker: staker, + signer: signer, + reward-cycle: reward-cycle, + bond-index: bond-index, + }) + ) +) + +(define-read-only (get-staker-unclaimed-rewards-for-cycle + (signer principal) + (reward-cycle uint) + (bond-index (optional uint)) + (staker principal) + ) + (default-to u0 + (map-get? staker-unclaimed-rewards-for-cycle { + staker: staker, + signer: signer, + reward-cycle: reward-cycle, + bond-index: bond-index, + }) + ) +) + +(define-read-only (get-signer-rewards-per-token-for-cycle + (signer principal) + (reward-cycle uint) + (bond-index (optional uint)) + ) + (default-to u0 + (map-get? signer-rewards-per-token-for-cycle { + signer: signer, + reward-cycle: reward-cycle, + bond-index: bond-index, }) ) ) @@ -2378,6 +3166,20 @@ (map-get? protocol-bonds bond-index) ) +;; Returns `true` if and only if the given staker has already successfully +;; called `announce-l1-early-exit` for the given bond index. +(define-read-only (has-announced-l1-early-exit + (bond-index uint) + (staker principal) + ) + (default-to false + (map-get? protocol-bond-l1-early-exit-announced { + bond-index: bond-index, + staker: staker, + }) + ) +) + ;; Returns the expected L1 unlock height for a given bond index. ;; This is equal to 1/2 of a reward cycle before the end of the bond period. (define-read-only (get-bond-l1-unlock-height (bond-index uint)) @@ -2390,6 +3192,21 @@ (var-get first-pox-5-reward-cycle) ) +;; Clamp a value between a min and max. +(define-read-only (clamp + (value uint) + (min uint) + (max uint) + ) + (if (> value max) + max + (if (< value min) + min + value + ) + ) +) + ;;; Contract caller allowances (define-read-only (check-caller-allowed) @@ -2414,7 +3231,15 @@ ;; Revoke contract-caller authorization to call stacking methods (define-public (disallow-contract-caller (caller principal)) (begin + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED_CALLER) + (print { + topic: "disallow-contract-caller", + sender: tx-sender, + contract-caller: caller, + }) (ok (map-delete allowance-contract-callers { sender: tx-sender, contract-caller: caller, @@ -2431,7 +3256,16 @@ (until-burn-ht (optional uint)) ) (begin + ;; ensure no reentrancy through signer-manager trait calls + (try! (validate-no-reentrancy)) + (asserts! (is-eq tx-sender contract-caller) ERR_UNAUTHORIZED_CALLER) + (print { + topic: "allow-contract-caller", + sender: tx-sender, + contract-caller: caller, + until-burn-ht: until-burn-ht, + }) (ok (map-set allowance-contract-callers { sender: tx-sender, contract-caller: caller, @@ -2525,15 +3359,6 @@ (cycle uint) ) (let ((last-item (map-get? signer-set-ll-last-for-cycle cycle))) - ;; Todo: remove this and guard in a higher-level fn - (asserts! - (not (is-some (map-get? signer-set-ll-for-cycle { - cycle: cycle, - signer: signer, - }))) - ERR_ALREADY_STAKED - ) - (match last-item last-signer (let ((last-node (unwrap-panic (map-get? signer-set-ll-for-cycle { cycle: cycle, @@ -2568,7 +3393,6 @@ ) (map-set signer-set-ll-last-for-cycle cycle signer) - (ok true) ) ) @@ -2768,40 +3592,50 @@ ;; Contruct an L1 lockup script. ;; -;; `unlock-bytes` and `early-unlock-bytes` are caller-supplied Bitcoin -;; Script *subscripts*. `unlock-bytes` should be a subscript that validates the -;; signature of the staker (e.g., ` OP_CHECKSIG` or an M-of-N -;; `CHECKMULTISIG` template). It MUST leave a valid result on the stack. +;; `staker-unlock-bytes` and `early-unlock-bytes` are caller-supplied Bitcoin Script +;; *subscripts*. Both MUST leave a valid (boolean) result on the stack. +;; `staker-unlock-bytes` should be a subscript that validates the signature of the +;; staker (e.g., ` OP_CHECKSIG` or an M-of-N `CHECKMULTISIG` template); +;; it always runs and its result is the final result of the script. ;; `early-unlock-bytes` should be a subscript that validates the signature of -;; the early unlock admin and MUST NOT leave anything on the stack (e.g. -;; ` OP_CHECKSIGVERIFY`, or an M-of-N `CHECKMULTISIGVERIFY` template). +;; the early-unlock key for the early-exit branch (e.g., ` OP_CHECKSIG`, +;; or an M-of-N `CHECKMULTISIG` template); its result is consumed by the shared +;; OP_VERIFY. +;; +;; The staker is bound to the script via a hashed commitment rather than a +;; cleartext push: the OP_ELSE branch requires revealing the 32-byte +;; `sha256(to-consensus-buff? staker)` preimage of the committed hash +;; ` = sha256(sha256(to-consensus-buff? staker))`. ;; ;; The constructed script has this structure: ;; ``` -;; OP_DROP ;; OP_IF -;; OP_CHECKLOCKTIMEVERIFY OP_DROP -;; +;; OP_CHECKLOCKTIMEVERIFY ;; OP_ELSE +;; OP_SIZE <32> OP_EQUALVERIFY +;; OP_SHA256 OP_EQUALVERIFY ;; -;; ;; OP_ENDIF +;; OP_VERIFY +;; ;; ``` (define-read-only (construct-lockup-script (staker principal) (unlock-burn-height uint) - (unlock-bytes (buff 683)) + (staker-unlock-bytes (buff 683)) (early-unlock-bytes (buff 683)) ) - (concat (push-script-bytes (unwrap-panic (to-consensus-buff? staker))) - (concat 0x7563 ;; OP_DROP, OP_IF - (concat (push-c-script-num unlock-burn-height) - (concat 0xb175 ;; OP_CHECKLOCKTIMEVERIFY, OP_DROP - (concat unlock-bytes - (concat 0x67 ;; OP_ELSE + (concat 0x63 ;; OP_IF + (concat (push-c-script-num unlock-burn-height) + (concat 0xb167 ;; OP_CHECKLOCKTIMEVERIFY, OP_ELSE + (concat 0x82012088a820 + ;; OP_SIZE, <32>, OP_EQUALVERIFY, OP_SHA256, OP_PUSHBYTES_32 + (concat + (sha256 (sha256 (unwrap-panic (to-consensus-buff? staker)))) + (concat 0x88 ;; OP_EQUALVERIFY (concat early-unlock-bytes - (concat unlock-bytes 0x68 - ;; OP_ENDIF + (concat 0x6869 ;; OP_ENDIF, OP_VERIFY + staker-unlock-bytes )) )) )) @@ -2812,11 +3646,11 @@ (define-read-only (construct-lockup-output-script (staker principal) (unlock-burn-height uint) - (unlock-bytes (buff 683)) + (staker-unlock-bytes (buff 683)) (early-unlock-bytes (buff 683)) ) (concat 0x0020 - (sha256 (construct-lockup-script staker unlock-burn-height unlock-bytes + (sha256 (construct-lockup-script staker unlock-burn-height staker-unlock-bytes early-unlock-bytes )) ) From d3a88103cbceff0a2276b27d4189fcfb9b79cb46 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:58:44 -0700 Subject: [PATCH 27/30] fix: fund utxo for signers --- stacking/btc-staker.ts | 48 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 67c6559..65b7fb0 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -35,6 +35,7 @@ import { import { signSignerKeyGrant, pox5, pox5Signer, clarigenClient } from './pox-5-helpers.js'; import { readFile } from 'node:fs/promises'; import { buildSbtcDepositAddress, REGTEST, SbtcApiClientDevenv } from 'sbtc'; +import { p2tr, TEST_NETWORK } from '@scure/btc-signer'; const stakingInterval = parseEnvInt('STACKING_INTERVAL', true); const stakingCyclesPox5 = parseEnvInt('STACKING_CYCLES_POX_5', true); @@ -159,6 +160,7 @@ async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockB const grantedSignerKeys = new Set(); const depositedSBTC = new Set(); +const fundedSignerKeys = new Set(); let hasDeployedSBTC = false; async function run() { @@ -255,6 +257,8 @@ async function run() { grantedSignerKeys.add(account.signerManager); } + await fundSbtcSignerUtxo(); + if (!depositedSBTC.has(account.stxAddress)) { await depositSBTC(account); depositedSBTC.add(account.stxAddress); @@ -294,14 +298,50 @@ async function run() { await Promise.all(txIdsToWait.map(waitForTxConfirmed)); } -async function depositSBTC(account: Account) { - console.log('Depositing sBTC for account:', account.stxAddress); - const client = new SbtcApiClientDevenv({ +function getSbtcClient() { + return new SbtcApiClientDevenv({ sbtcContract: sbtcDeployerAddress, btcApiUrl: 'http://bitcoind:18443', stxApiUrl: 'http://stacks-api:3999', sbtcApiUrl: 'http://emily-server:3031', }); +} + +async function fundSbtcSignerUtxo() { + const client = getSbtcClient(); + let signerKey = ''; + try { + signerKey = await client.fetchSignersPublicKey(); + // oxlint-disable-next-line no-unused-vars + } catch (_error) { + return; + } + if (fundedSignerKeys.has(signerKey)) return; + + const regtest = { ...TEST_NETWORK, bech32: 'bcrt' }; + const xOnlyPublicKey = + signerKey.length === 66 ? signerKey.slice(2) : Buffer.from(signerKey).toString('hex'); + if (xOnlyPublicKey.length !== 64) { + throw new Error( + `Expected 32-byte x-only sBTC signer key, got ${xOnlyPublicKey.length} hex chars` + ); + } + const signerPayment = p2tr(xOnlyPublicKey, undefined, regtest); + if (!signerPayment.address) { + throw new Error(`Could not derive sBTC signer address for aggregate key ${signerKey}`); + } + + const txid = await sendToAddress(WALLET_NAME, signerPayment.address, 0.1); + logger.info( + { txid, address: signerPayment.address, aggregateKey: signerKey }, + 'Funded sBTC signer UTXO' + ); + fundedSignerKeys.add(signerKey); +} + +async function depositSBTC(account: Account) { + console.log('Depositing sBTC for account:', account.stxAddress); + const client = getSbtcClient(); // 1. Build the sBTC deposit address const deposit = buildSbtcDepositAddress({ stacksAddress: account.stxAddress, // the address to send/mint the sBTC to @@ -309,7 +349,7 @@ async function depositSBTC(account: Account) { reclaimLockTime: 950, // default locktime for reclaiming failed deposits reclaimPublicKey: account.pubKey.slice(0, 64), // public key for reclaiming failed deposits network: REGTEST, - maxSignerFee: 1000, // max fee the signers can charge for processing the subsequent sweep tx + maxSignerFee: 50_000, // max fee the signers can charge for processing the subsequent sweep tx }); // console.log('Deposit Script:', deposit.depositScript); From 968281da928b2fd487e8d73292e98467ae48be26 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:54:51 -0700 Subject: [PATCH 28/30] fix: add emily&sbtc event observers, fix aggregate pubkey handling --- docker-compose.yml | 2 +- stacking/btc-staker.ts | 26 +++++++++++++++++--------- stacks-krypton-miner.toml | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f367bb2..cfd9084 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ x-common-vars: - &BITCOIN_RPC_PASS btc - &MINE_INTERVAL ${MINE_INTERVAL:-1s} - &MINE_INTERVAL_EPOCH25 ${MINE_INTERVAL_EPOCH25:-2s} # bitcoin block times in epoch 2.5 - - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-1s} # bitcoin block times in epoch 3 + - &MINE_INTERVAL_EPOCH3 ${MINE_INTERVAL_EPOCH3:-2s} # bitcoin block times in epoch 3 - &MINE_INTERVAL_EPOCH40 ${MINE_INTERVAL_EPOCH40:-2s} # bitcoin block times in epoch 4 - &NAKAMOTO_BLOCK_INTERVAL 2 # seconds to wait between issuing stx-transfer transactions (which triggers Nakamoto block production) - &STACKS_20_HEIGHT ${STACKS_20_HEIGHT:-0} diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 65b7fb0..23517e3 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -5,7 +5,7 @@ import { makeContractDeploy, } from '@stacks/transactions'; import { hex } from '@scure/base'; -import { PoxInfo } from '@stacks/stacking'; +import { PoxInfo, V2PoxInfoResponse } from '@stacks/stacking'; import { accounts, parseEnvInt, @@ -164,7 +164,13 @@ const fundedSignerKeys = new Set(); let hasDeployedSBTC = false; async function run() { - const poxInfo = await accounts[0]!.client.getPoxInfo(); + let poxInfo: V2PoxInfoResponse; + try { + poxInfo = await accounts[0]!.client.getPoxInfo(); + // oxlint-disable-next-line no-unused-vars + } catch (error) { + return; + } if (poxInfo.current_burnchain_block_height! > EPOCH_30_START + 1 && !hasDeployedSBTC) { await deploySBTC(accounts[0]!); @@ -319,8 +325,14 @@ async function fundSbtcSignerUtxo() { if (fundedSignerKeys.has(signerKey)) return; const regtest = { ...TEST_NETWORK, bech32: 'bcrt' }; - const xOnlyPublicKey = - signerKey.length === 66 ? signerKey.slice(2) : Buffer.from(signerKey).toString('hex'); + const signerKeyHex = signerKey.startsWith('0x') ? signerKey.slice(2) : signerKey; + const xOnlyPublicKey = (() => { + if (signerKeyHex.length === 64) return signerKeyHex; + if (signerKeyHex.length === 66) return signerKeyHex.slice(2); + if (signerKeyHex.length === 128) return signerKeyHex.slice(0, 64); + if (signerKeyHex.length === 130 && signerKeyHex.startsWith('04')) return signerKeyHex.slice(2, 66); + return Buffer.from(signerKey).toString('hex'); + })(); if (xOnlyPublicKey.length !== 64) { throw new Error( `Expected 32-byte x-only sBTC signer key, got ${xOnlyPublicKey.length} hex chars` @@ -342,7 +354,7 @@ async function fundSbtcSignerUtxo() { async function depositSBTC(account: Account) { console.log('Depositing sBTC for account:', account.stxAddress); const client = getSbtcClient(); - // 1. Build the sBTC deposit address + const deposit = buildSbtcDepositAddress({ stacksAddress: account.stxAddress, // the address to send/mint the sBTC to signersPublicKey: await client.fetchSignersPublicKey(), // the aggregated public key of the signers @@ -351,10 +363,6 @@ async function depositSBTC(account: Account) { network: REGTEST, maxSignerFee: 50_000, // max fee the signers can charge for processing the subsequent sweep tx }); - - // console.log('Deposit Script:', deposit.depositScript); - // console.log('Reclaim Script:', deposit.reclaimScript); - // console.log('P2TR Output:', deposit.trOut); console.log('Deposit Address:', { address: deposit.address, account: account.stxAddress }); const txid = await sendToAddress(WALLET_NAME, deposit.address, 0.1); diff --git a/stacks-krypton-miner.toml b/stacks-krypton-miner.toml index 743ae00..e31a570 100644 --- a/stacks-krypton-miner.toml +++ b/stacks-krypton-miner.toml @@ -60,6 +60,34 @@ events_keys = ["stackerdb", "block_proposal", "burn_blocks"] endpoint = "stacks-signer-3:30003" events_keys = ["stackerdb", "block_proposal", "burn_blocks"] +[[events_observer]] +endpoint = "emily-sidecar:20540" +events_keys = [ + "$SBTC_DEPLOYER_ADDRESS.sbtc-registry::print", +] +timeout_ms = 10_000 + +[[events_observer]] +endpoint = "sbtc-signer-1:8801" +events_keys = [ + "$SBTC_DEPLOYER_ADDRESS.sbtc-registry::print", +] +timeout_ms = 10_000 + +[[events_observer]] +endpoint = "sbtc-signer-2:8801" +events_keys = [ + "$SBTC_DEPLOYER_ADDRESS.sbtc-registry::print", +] +timeout_ms = 10_000 + +[[events_observer]] +endpoint = "sbtc-signer-3:8801" +events_keys = [ + "$SBTC_DEPLOYER_ADDRESS.sbtc-registry::print", +] +timeout_ms = 10_000 + [burnchain] chain = "bitcoin" mode = "krypton" From 75ebf7dd95200d8aed529584a7dc0886d8c4584b Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:12:14 -0700 Subject: [PATCH 29/30] feat: add calculate-rewards to btc-staker script --- stacking/btc-staker.ts | 77 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index 23517e3..f52cdd2 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -163,6 +163,78 @@ const depositedSBTC = new Set(); const fundedSignerKeys = new Set(); let hasDeployedSBTC = false; +async function maybeCalculateRewards(account: Account) { + const pox5Info = await clarigenClient.ro(pox5.getPoxInfo()); + if (!pox5Info.value) return; + + const cycleLength = pox5Info.value.rewardCycleLength; + const firstBurnHeight = pox5Info.value.firstBurnchainBlockHeight; + const currentBurnHeight = BigInt( + (await account.client.getPoxInfo()).current_burnchain_block_height! + ); + const distributionLength = cycleLength / 2n; + const currentDistributionCycle = (currentBurnHeight - firstBurnHeight) / distributionLength; + if (currentDistributionCycle === 0n) return; + + const calculationHeight = firstBurnHeight + currentDistributionCycle * distributionLength - 1n; + const lastCalculationHeight = await clarigenClient.ro(pox5.getLastRewardComputeHeight()); + if (calculationHeight <= lastCalculationHeight) return; + + const calculationRewardCycle = (calculationHeight - firstBurnHeight) / cycleLength; + const firstBondCycle = await clarigenClient.ro(pox5.getFirstPox5RewardCycle()); + const latestBondIndex = + calculationRewardCycle <= firstBondCycle ? 0n : (calculationRewardCycle - firstBondCycle) / 2n; + const bondPeriods = ( + await Promise.all( + Array.from({ length: 6 }, async (_, offset) => { + const bondIndex = latestBondIndex - BigInt(offset); + if (bondIndex < 0n) return null; + const bond = await clarigenClient.ro(pox5.getProtocolBond(bondIndex)); + if (!bond) return null; + const bondStartHeight = firstBurnHeight + (firstBondCycle + bondIndex * 2n) * cycleLength; + const bondEndHeight = + firstBurnHeight + (firstBondCycle + (bondIndex + 6n) * 2n) * cycleLength; + if (calculationHeight <= bondStartHeight || calculationHeight > bondEndHeight) return null; + return { bondIndex, stxValueRatio: bond.stxValueRatio }; + }) + ) + ) + .filter((bond): bond is { bondIndex: bigint; stxValueRatio: bigint } => bond !== null) + .sort((a, b) => { + if (a.stxValueRatio === b.stxValueRatio) return a.bondIndex < b.bondIndex ? -1 : 1; + return a.stxValueRatio > b.stxValueRatio ? -1 : 1; + }) + .map(bond => bond.bondIndex); + + const tx = await makeContractCall({ + ...pox5.calculateRewards({ bondPeriods }), + senderKey: account.privKey, + network, + fee: getNextTxFee(), + nonce: (await fetchAccount(account.stxAddress)).nonce, + }); + const result = await broadcastTransaction({ + transaction: tx, + network, + }); + if ('reason' in result) { + account.logger.error( + { ...result, calculationHeight: calculationHeight.toString() }, + `Error calculating rewards: ${result.reason}` + ); + throw new Error(`Error calculating rewards: ${result.reason}`); + } + account.logger.info( + { + txid: result.txid, + calculationHeight: calculationHeight.toString(), + bondPeriods: bondPeriods.map(String), + }, + 'calculate-rewards tx broadcast' + ); + await waitForTxConfirmed(result.txid); +} + async function run() { let poxInfo: V2PoxInfoResponse; try { @@ -183,6 +255,8 @@ async function run() { const currentCycle = poxInfo.reward_cycle_id; + await maybeCalculateRewards(accounts[0]!); + const accountInfos = await Promise.all( accounts.map(async a => { const info = await fetchAccount(a.stxAddress); @@ -330,7 +404,8 @@ async function fundSbtcSignerUtxo() { if (signerKeyHex.length === 64) return signerKeyHex; if (signerKeyHex.length === 66) return signerKeyHex.slice(2); if (signerKeyHex.length === 128) return signerKeyHex.slice(0, 64); - if (signerKeyHex.length === 130 && signerKeyHex.startsWith('04')) return signerKeyHex.slice(2, 66); + if (signerKeyHex.length === 130 && signerKeyHex.startsWith('04')) + return signerKeyHex.slice(2, 66); return Buffer.from(signerKey).toString('hex'); })(); if (xOnlyPublicKey.length !== 64) { From ed6d503b3339b826b881a794da51a0255dace200 Mon Sep 17 00:00:00 2001 From: Hank Stoever <233293993+hstove-stacks@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:16:20 -0700 Subject: [PATCH 30/30] fix: remove unused sbtc contracts --- stacking/btc-staker.ts | 49 --- ...KAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar | 369 ------------------ ...XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar | 160 -------- ...FAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar | 310 --------------- 4 files changed, 888 deletions(-) delete mode 100644 stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar delete mode 100644 stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar delete mode 100644 stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar diff --git a/stacking/btc-staker.ts b/stacking/btc-staker.ts index f52cdd2..dfc1458 100644 --- a/stacking/btc-staker.ts +++ b/stacking/btc-staker.ts @@ -161,7 +161,6 @@ async function submitBtcLock(account: Account, unlockBurnHeight: bigint, unlockB const grantedSignerKeys = new Set(); const depositedSBTC = new Set(); const fundedSignerKeys = new Set(); -let hasDeployedSBTC = false; async function maybeCalculateRewards(account: Account) { const pox5Info = await clarigenClient.ro(pox5.getPoxInfo()); @@ -243,11 +242,6 @@ async function run() { } catch (error) { return; } - - if (poxInfo.current_burnchain_block_height! > EPOCH_30_START + 1 && !hasDeployedSBTC) { - await deploySBTC(accounts[0]!); - hasDeployedSBTC = true; - } if (poxInfo.current_burnchain_block_height! < EPOCH_40_START) { // logger.info({ burnHeight: poxInfo.current_burnchain_block_height }, 'Not on epoch 3.5 yet, skipping'); return; @@ -463,49 +457,6 @@ async function depositSBTC(account: Account) { console.log('Notified sbtc:', { notifyResult, txid }); } -async function deploySBTC(account: Account) { - console.log('Skipping sBTC Deployment'); - return; - const registry = await readFile( - 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar', - 'utf8' - ); - const token = await readFile( - 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar', - 'utf8' - ); - const withdrawal = await readFile( - 'contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar', - 'utf8' - ); - - async function deployContract(contract: string, name: string) { - const deployTx = await makeContractDeploy({ - senderKey: accounts[0]!.privKey, - network, - contractName: name, - codeBody: contract, - clarityVersion: 3, - }); - const deployResult = await broadcastTransaction({ - transaction: deployTx, - network, - }); - if ('reason' in deployResult) { - if (deployResult.reason === 'ContractAlreadyExists') { - return; - } - throw new Error(`Error deploying sbtc contract: ${deployResult.reason}`); - } - account.logger.info({ ...deployResult, contractName: name }, 'Deployed sbtc contract'); - await waitForTxConfirmed(deployResult.txid); - } - - await deployContract(registry, 'sbtc-registry'); - await deployContract(token, 'sbtc-token'); - await deployContract(withdrawal, 'sbtc-withdrawal'); -} - async function loop() { await waitForSetup(); await initBtcWallet(); diff --git a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar deleted file mode 100644 index b3f5672..0000000 --- a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry.clar +++ /dev/null @@ -1,369 +0,0 @@ -;; sBTC Registry contract - -;; Error codes -(define-constant ERR_UNAUTHORIZED (err u400)) -(define-constant ERR_INVALID_REQUEST_ID (err u401)) -(define-constant ERR_AGG_PUBKEY_REPLAY (err u402)) - -;; Protocol contract type -(define-constant governance-role 0x00) -(define-constant deposit-role 0x01) -(define-constant withdrawal-role 0x02) - -;; Variables -(define-data-var last-withdrawal-request-id uint u0) -(define-data-var current-signature-threshold uint u0) -(define-data-var current-signer-set (list 128 (buff 33)) (list)) -(define-data-var current-aggregate-pubkey (buff 33) 0x02158613a973bb4469dc9713e0a330a30b6cb88580b772658990a0b052149ca42a) -(define-data-var current-signer-principal principal tx-sender) - -;; Maps -;; Active protocol contracts -(define-map active-protocol-contracts (buff 1) principal) -(map-set active-protocol-contracts governance-role .sbtc-bootstrap-signers) -(map-set active-protocol-contracts deposit-role .sbtc-deposit) -(map-set active-protocol-contracts withdrawal-role .sbtc-withdrawal) -;; Role for active protocol contracts -(define-map active-protocol-roles principal (buff 1)) -(map-set active-protocol-roles .sbtc-bootstrap-signers governance-role) -(map-set active-protocol-roles .sbtc-deposit deposit-role) -(map-set active-protocol-roles .sbtc-withdrawal withdrawal-role) -;; Internal data structure to store withdrawal -;; requests. Requests are associated with a unique -;; request ID. -(define-map withdrawal-requests uint { - ;; Amount of sBTC being withdrawaled (in sats) - amount: uint, - max-fee: uint, - sender: principal, - ;; BTC recipient address in the same format of - ;; pox contracts - recipient: { - version: (buff 1), - hashbytes: (buff 32), - }, - ;; Burn block height where the withdrawal request was - ;; created - block-height: uint, -}) - -;; Data structure to map request-id to status -;; If status is `none`, the request is pending. -;; Otherwise, the boolean value indicates whether -;; the withdrawal was accepted. -(define-map withdrawal-status uint bool) - -;; Data structure to map successful withdrawal requests -;; to their respective sweep transaction. Stores the -;; txid, burn hash, and burn height. -(define-map completed-withdrawal-sweep uint { - sweep-txid: (buff 32), - sweep-burn-hash: (buff 32), - sweep-burn-height: uint, -}) - -;; Internal data structure to store completed -;; deposit requests & avoid replay attacks. -(define-map deposit-status {txid: (buff 32), vout-index: uint} bool) - -;; Data structure to map successful deposit requests -;; to their respective sweep transaction. Stores the -;; txid, burn hash, and burn height. -(define-map completed-deposits {txid: (buff 32), vout-index: uint} - { - amount: uint, - recipient: principal, - sweep-txid: (buff 32), - sweep-burn-hash: (buff 32), - sweep-burn-height: uint, - } -) - -;; Data structure to store aggregate pubkey, -;; stored to avoid replay -(define-map aggregate-pubkeys (buff 33) bool) - -;; Read-only functions -;; Get a withdrawal request by its ID. -;; This function returns the fields of the withdrawal -;; request, along with its status. -(define-read-only (get-withdrawal-request (id uint)) - (match (map-get? withdrawal-requests id) - request (some (merge request { - status: (map-get? withdrawal-status id) - })) - none - ) -) - -;; Get a completed withdrawal sweep data by its request ID. -;; This function returns the fields of the withdrawal-sweeps map. -(define-read-only (get-completed-withdrawal-sweep-data (id uint)) - (map-get? completed-withdrawal-sweep id) -) - -;; Get a completed deposit by its transaction ID & vout index. -;; This function returns the fields of the completed-deposits map. -(define-read-only (get-completed-deposit (txid (buff 32)) (vout-index uint)) - (map-get? completed-deposits {txid: txid, vout-index: vout-index}) -) - -;; Get a completed deposit sweep data by its transaction ID & vout index. -;; This function returns the fields of the completed-deposits map. -(define-read-only (get-deposit-status (txid (buff 32)) (vout-index uint)) - (map-get? deposit-status {txid: txid, vout-index: vout-index}) -) - -;; Get the current signer set. -;; This function returns the current signer set as a list of principals. -(define-read-only (get-current-signer-data) - { - current-signer-set: (var-get current-signer-set), - current-aggregate-pubkey: (var-get current-aggregate-pubkey), - current-signer-principal: (var-get current-signer-principal), - current-signature-threshold: (var-get current-signature-threshold), - } -) - -;; Get the current aggregate pubkey. -;; This function returns the current aggregate pubkey. -(define-read-only (get-current-aggregate-pubkey) - (var-get current-aggregate-pubkey) -) - -;; Get the current signer principal. -;; This function returns the current signer principal. -(define-read-only (get-current-signer-principal) - (var-get current-signer-principal) -) - -(define-read-only (get-current-signer-set) - (var-get current-signer-set) -) - -(define-read-only (get-active-protocol (contract-flag (buff 1))) - (map-get? active-protocol-contracts contract-flag) -) - - -;; Public functions - -;; Store a new withdrawal request. -;; Note that this function can only be called by other sBTC -;; contracts - it cannot be called by users directly. -;; -;; This function does not handle validation or moving the funds. -;; Instead, it is purely for the purpose of storing the request. -;; -;; The function will emit a print event with the topic "withdrawal-create" -;; and the data of the request. -(define-public (create-withdrawal-request - (amount uint) - (max-fee uint) - (sender principal) - (recipient { version: (buff 1), hashbytes: (buff 32) }) - (height uint) - ) - (let - ( - (id (increment-last-withdrawal-request-id)) - ) - (try! (is-protocol-caller withdrawal-role contract-caller)) - ;; #[allow(unchecked_data)] - (map-insert withdrawal-requests id { - amount: amount, - max-fee: max-fee, - sender: sender, - recipient: recipient, - block-height: height, - }) - (print { - topic: "withdrawal-create", - amount: amount, - request-id: id, - sender: sender, - recipient: recipient, - block-height: height, - max-fee: max-fee, - }) - (ok id) - ) -) - -;; Complete withdrawal request by noting the acceptance in the -;; withdrawal-status state map. -;; -;; This function will emit a print event with the topic -;; "withdrawal-accept". -(define-public (complete-withdrawal-accept - (request-id uint) - (bitcoin-txid (buff 32)) - (output-index uint) - (signer-bitmap uint) - (fee uint) - (burn-hash (buff 32)) - (burn-height uint) - (sweep-txid (buff 32)) - ) - (begin - (try! (is-protocol-caller withdrawal-role contract-caller)) - ;; Mark the withdrawal as completed - (map-insert withdrawal-status request-id true) - (map-insert completed-withdrawal-sweep request-id { - sweep-txid: sweep-txid, - sweep-burn-hash: burn-hash, - sweep-burn-height: burn-height, - }) - (print { - topic: "withdrawal-accept", - request-id: request-id, - bitcoin-txid: bitcoin-txid, - signer-bitmap: signer-bitmap, - output-index: output-index, - fee: fee, - burn-hash: burn-hash, - burn-height: burn-height, - sweep-txid: sweep-txid, - }) - (ok true) - ) -) - -;; Complete withdrawal request by noting the rejection in the -;; withdrawal-status state map. -;; -;; This function will emit a print event with the topic -;; "withdrawal-reject". -(define-public (complete-withdrawal-reject - (request-id uint) - (signer-bitmap uint) - ) - (begin - (try! (is-protocol-caller withdrawal-role contract-caller)) - ;; Mark the withdrawal as completed - (map-insert withdrawal-status request-id false) - (print { - topic: "withdrawal-reject", - request-id: request-id, - signer-bitmap: signer-bitmap, - }) - (ok true) - ) -) - -;; Store a new insert request. -;; Note that this function can only be called by other sBTC -;; contracts (specifically the current version of the deposit contract) -;; - it cannot be called by users directly. -;; -;; This function does not handle validation or moving the funds. -;; Instead, it is purely for the purpose of storing the completed deposit. -(define-public (complete-deposit - (txid (buff 32)) - (vout-index uint) - (amount uint) - (recipient principal) - (burn-hash (buff 32)) - (burn-height uint) - (sweep-txid (buff 32)) - ) - (begin - (try! (is-protocol-caller deposit-role contract-caller)) - (map-insert deposit-status {txid: txid, vout-index: vout-index} true) - (map-insert completed-deposits {txid: txid, vout-index: vout-index} { - amount: amount, - recipient: recipient, - sweep-txid: sweep-txid, - sweep-burn-hash: burn-hash, - sweep-burn-height: burn-height, - }) - (print { - topic: "completed-deposit", - bitcoin-txid: txid, - output-index: vout-index, - amount: amount, - burn-hash: burn-hash, - burn-height: burn-height, - sweep-txid: sweep-txid, - }) - (ok true) - ) -) - -;; Rotate the signer set, multi-sig principal, & aggregate pubkey -;; This function can only be called by the bootstrap-signers contract. -(define-public (rotate-keys - (new-keys (list 128 (buff 33))) - (new-address principal) - (new-aggregate-pubkey (buff 33)) - (new-signature-threshold uint) - ) - (begin - ;; Check that caller is protocol contract - (try! (is-protocol-caller governance-role contract-caller)) - ;; Check that the aggregate pubkey is not already in the map - (asserts! (map-insert aggregate-pubkeys new-aggregate-pubkey true) ERR_AGG_PUBKEY_REPLAY) - ;; Update the current signer set - (var-set current-signer-set new-keys) - ;; Update the current multi-sig address - (var-set current-signer-principal new-address) - ;; Update the current signature threshold - (var-set current-signature-threshold new-signature-threshold) - ;; Update the current aggregate pubkey - (var-set current-aggregate-pubkey new-aggregate-pubkey) - (print { - topic: "key-rotation", - new-keys: new-keys, - new-address: new-address, - new-aggregate-pubkey: new-aggregate-pubkey, - new-signature-threshold: new-signature-threshold - }) - (ok true) - ) -) - -;; Update protocol contract -;; This function can only be called by the active bootstrap-signers contract -(define-public (update-protocol-contract - (contract-type (buff 1)) - (new-contract principal) - ) - (begin - ;; Check that caller is protocol contract - (try! (is-protocol-caller governance-role contract-caller)) - ;; Update the protocol contract - (map-set active-protocol-contracts contract-type new-contract) - ;; Update the protocol role - (map-set active-protocol-roles new-contract contract-type) - (print { - topic: "update-protocol-contract", - contract-type: contract-type, - new-contract: new-contract, - }) - (ok true) - ) -) - -;; Private functions -;; Increment the last withdrawal request ID and -;; return the new value. -(define-private (increment-last-withdrawal-request-id) - (let - ( - (next-value (+ u1 (var-get last-withdrawal-request-id))) - ) - (var-set last-withdrawal-request-id next-value) - next-value - ) -) - -;; Checks whether the contract-caller is a protocol contract -(define-read-only (is-protocol-caller (contract-flag (buff 1)) (contract principal)) - (begin - ;; Check that contract-caller is an protocol contract - (asserts! (is-eq (some contract) (map-get? active-protocol-contracts contract-flag)) ERR_UNAUTHORIZED) - ;; Check that flag matches the contract-caller - (asserts! (is-eq (some contract-flag) (map-get? active-protocol-roles contract)) ERR_UNAUTHORIZED) - (ok true) - ) -) diff --git a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar deleted file mode 100644 index dcbb947..0000000 --- a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.clar +++ /dev/null @@ -1,160 +0,0 @@ -(define-constant ERR_NOT_OWNER (err u4)) ;; `tx-sender` or `contract-caller` tried to move a token it does not own. -(define-constant ERR_TRANSFER_INDEX_PREFIX u1000) - -(define-fungible-token sbtc-token) -(define-fungible-token sbtc-token-locked) - -(define-data-var token-name (string-ascii 32) "sBTC") -(define-data-var token-symbol (string-ascii 10) "sBTC") -(define-data-var token-uri (optional (string-utf8 256)) (some u"https://ipfs.io/ipfs/bafkreibqnozdui4ntgoh3oo437lvhg7qrsccmbzhgumwwjf2smb3eegyqu")) -(define-constant token-decimals u8) - -(define-read-only (get-current-aggregate-pubkey) 0x0204cff1ade0cc7f74d1b5a2b7c7bee653cfb5e6c0dce360795d314c829c4aaf52) - -;; --- Protocol functions - -(define-public (protocol-lock (amount uint) (owner principal) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (try! (ft-burn? sbtc-token amount owner)) - (ft-mint? sbtc-token-locked amount owner) - ) -) - -(define-public (protocol-unlock (amount uint) (owner principal) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (try! (ft-burn? sbtc-token-locked amount owner)) - (ft-mint? sbtc-token amount owner) - ) -) - -(define-public (protocol-mint (amount uint) (recipient principal) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (ft-mint? sbtc-token amount recipient) - ) -) - -(define-public (protocol-burn (amount uint) (owner principal) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (ft-burn? sbtc-token amount owner) - ) -) - -(define-public (protocol-burn-locked (amount uint) (owner principal) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (ft-burn? sbtc-token-locked amount owner) - ) -) - -(define-public (protocol-set-name (new-name (string-ascii 32)) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (ok (var-set token-name new-name)) - ) -) - -(define-public (protocol-set-symbol (new-symbol (string-ascii 10)) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (ok (var-set token-symbol new-symbol)) - ) -) - -(define-public (protocol-set-token-uri (new-uri (optional (string-utf8 256))) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (ok (var-set token-uri new-uri)) - ) -) - -(define-private (protocol-mint-many-iter (item {amount: uint, recipient: principal})) - (ft-mint? sbtc-token (get amount item) (get recipient item)) -) - -(define-public (protocol-mint-many (recipients (list 200 {amount: uint, recipient: principal})) (contract-flag (buff 1))) - (begin - (try! (contract-call? .sbtc-registry is-protocol-caller contract-flag contract-caller)) - (ok (map protocol-mint-many-iter recipients)) - ) -) - -;; --- Public functions -(define-public (transfer-many - (recipients (list 200 { - amount: uint, - sender: principal, - to: principal, - memo: (optional (buff 34)) }))) - (fold transfer-many-iter recipients (ok u0)) -) - -(define-private (transfer-many-iter - (individual-transfer { - amount: uint, - sender: principal, - to: principal, - memo: (optional (buff 34)) }) - (result (response uint uint))) - (match result - index - (begin - (unwrap! - (transfer - (get amount individual-transfer) - (get sender individual-transfer) - (get to individual-transfer) - (get memo individual-transfer)) - (err (+ ERR_TRANSFER_INDEX_PREFIX index))) - (ok (+ index u1)) - ) - err-index - (err err-index) - ) -) - -;; sip-010-trait - -(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) - (begin - (asserts! (or (is-eq tx-sender sender) (is-eq contract-caller sender)) ERR_NOT_OWNER) - (try! (ft-transfer? sbtc-token amount sender recipient)) - (match memo to-print (print to-print) 0x) - (ok true) - ) -) - -(define-read-only (get-name) - (ok (var-get token-name)) -) - -(define-read-only (get-symbol) - (ok (var-get token-symbol)) -) - -(define-read-only (get-decimals) - (ok token-decimals) -) - -(define-read-only (get-balance (who principal)) - (ok (+ (ft-get-balance sbtc-token who) (ft-get-balance sbtc-token-locked who))) -) - -(define-read-only (get-balance-available (who principal)) - (ok (ft-get-balance sbtc-token who)) -) - -(define-read-only (get-balance-locked (who principal)) - (ok (ft-get-balance sbtc-token-locked who)) -) - -(define-read-only (get-total-supply) - (ok (+ (ft-get-supply sbtc-token) (ft-get-supply sbtc-token-locked))) -) - -(define-read-only (get-token-uri) - (ok (var-get token-uri)) -) \ No newline at end of file diff --git a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar b/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar deleted file mode 100644 index 22aaa77..0000000 --- a/stacking/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-withdrawal.clar +++ /dev/null @@ -1,310 +0,0 @@ -;; Error codes - -;; The `version` part of the recipient address is invalid -(define-constant ERR_INVALID_ADDR_VERSION (err u500)) -;; The `hashbytes` part of the recipient address is invalid -(define-constant ERR_INVALID_ADDR_HASHBYTES (err u501)) -;; The size of the withdrawal is smaller than the dust limit -(define-constant ERR_DUST_LIMIT (err u502)) -;; The request id was invalid / returned 'none' -(define-constant ERR_INVALID_REQUEST (err u503)) -;; The caller is not the currently-governing multisig principal -(define-constant ERR_INVALID_CALLER (err u504)) -;; The withdrawal request was already processed -(define-constant ERR_ALREADY_PROCESSED (err u505)) -;; The paid fee was higher than requested -(define-constant ERR_FEE_TOO_HIGH (err u506)) -;; The returned index marks the failed transaction in list -(define-constant ERR_WITHDRAWAL_INDEX_PREFIX (unwrap-err! ERR_WITHDRAWAL_INDEX (err true))) -(define-constant ERR_WITHDRAWAL_INDEX (err u507)) -(define-constant ERR_INVALID_BURN_HASH (err u508)) - -;; Maximum value of an address version as a uint -(define-constant MAX_ADDRESS_VERSION u6) -;; Maximum value of an address version that has a 20-byte hashbytes -;; (0x00, 0x01, 0x02, 0x03, and 0x04 have 20-byte hashbytes) -(define-constant MAX_ADDRESS_VERSION_BUFF_20 u4) -;; Maximum value of an address version that has a 32-byte hashbytes -;; (0x05 and 0x06 have 32-byte hashbytes) -(define-constant MAX_ADDRESS_VERSION_BUFF_32 u6) -;; The minimum amount of sBTC you can withdraw -(define-constant DUST_LIMIT u546) -;; protocol contract type -(define-constant withdraw-role 0x02) - -;; Initiate a new withdrawal request. -;; -;; # Notes -;; -;; ## Amounts -;; -;; This function locks up `amount + max-fee` from the tx-sender's account, -;; and when the withdrawal request is accepted, the signers will send -;; `amount` of sats to the recipient and spend an a fee amount to bitcoin -;; miners where fee less than or equal to max-fee. If fee is less than -;; max-fee, then the difference will be minted back to the user when -;; `accept-withdrawal-request` is invoked. -;; -;; ## The recipient -;; -;; This constraints and meaning of the recipient field is summarized as: -;; ```text -;; version == 0x00 and (len hashbytes) == 20 => P2PKH -;; version == 0x01 and (len hashbytes) == 20 => P2SH -;; version == 0x02 and (len hashbytes) == 20 => P2SH-P2WPKH -;; version == 0x03 and (len hashbytes) == 20 => P2SH-P2WSH -;; version == 0x04 and (len hashbytes) == 20 => P2WPKH -;; version == 0x05 and (len hashbytes) == 32 => P2WSH -;; version == 0x06 and (len hashbytes) == 32 => P2TR -;; ``` -;; Also see -;; -;; Below is a detailed breakdown of bitcoin address types and how they map -;; to the clarity value. In what follows below, the network used for the -;; human-readable parts is inherited from the network of the underlying -;; transaction itself (basically, on stacks mainnet we send to mainnet -;; bitcoin addresses and similarly on stacks testnet we send to bitcoin -;; testnet addresses). -;; -;; ### P2PKH -;; -;; Generally speaking, Pay-to-Public-Key-Hash addresses are formed by -;; taking the Hash160 of the public key, prefixing it with one byte (0x00 -;; on mainnet and 0x6F on testing) and then base58 encoding the result. -;; -;; To specify this address type as the recipient, the `version` is 0x00 and -;; the `hashbytes` is the Hash160 of the public key. -;; -;; -;; ### P2SH, P2SH-P2WPKH, and P2SH-P2WSH -;; -;; Pay-to-script-hash-* addresses are formed by taking the Hash160 of the -;; locking script, prefixing it with one byte (0x05 on mainnet and 0xC4 on -;; testnet) and base58 encoding the result. The difference between them -;; lies with the locking script. For P2SH-P2WPKH addresses, the locking -;; script is: -;; ```text -;; 0 || -;; ``` -;; For P2SH-P2WSH addresses, the locking script is: -;; ```text -;; 0 || -;; ``` -;; And for P2SH addresses you get to choose the locking script in its -;; entirety. -;; -;; Again, after you construct the locking script you take its Hash160, -;; prefix it with one byte and base58 encode it to form the address. To -;; specify these address types in the recipient, the `version` is 0x01, -;; 0x02, and 0x03 (for P2SH, P2SH-P2WPKH, and P2SH-P2WSH respectively) with -;; the `hashbytes` is the Hash160 of the locking script. -;; -;; -;; ### P2WPKH -;; -;; Pay-to-witness-public-key-hash addresses are formed by creating a -;; witness program made entirely of the Hash160 of the compressed public -;; key. -;; -;; To specify this address type in the recipient, the `version` is 0x04 and -;; the `hashbytes` is the Hash160 of the compressed public key. -;; -;; -;; ### P2WSH -;; -;; Pay-to-witness-script-hash addresses are formed by taking a witness -;; program that is compressed entirely of the SHA256 of the redeem script. -;; -;; To specify this address type in the recipient, the `version` is 0x05 and -;; the `hashbytes` is the SHA256 of the redeem script. -;; -;; -;; ### P2TR -;; -;; Pay-to-taproot addresses are formed by "tweaking" the x-coordinate of a -;; public key with a merkle tree. The result of the tweak is used as the -;; witness program for the address. -;; -;; To specify this address type in the recipient, the `version` is 0x06 and -;; the `hashbytes` is the "tweaked" public key. -(define-public (initiate-withdrawal-request (amount uint) - (recipient { version: (buff 1), hashbytes: (buff 32) }) - (max-fee uint) - ) - (begin - (try! (contract-call? .sbtc-token protocol-lock (+ amount max-fee) tx-sender withdraw-role)) - (asserts! (> amount DUST_LIMIT) ERR_DUST_LIMIT) - - ;; Validate the recipient address - (try! (validate-recipient recipient)) - - (ok (try! (contract-call? .sbtc-registry create-withdrawal-request amount max-fee tx-sender recipient burn-block-height))) - ) -) - -;; Accept a withdrawal request -(define-public (accept-withdrawal-request (request-id uint) - (bitcoin-txid (buff 32)) - (signer-bitmap uint) - (output-index uint) - (fee uint) - (burn-hash (buff 32)) - (burn-height uint) - (sweep-txid (buff 32))) - (let - ( - (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) - (request (unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST)) - (requested-max-fee (get max-fee request)) - (requested-amount (get amount request)) - (requester (get sender request)) - ) - - ;; Verify that Bitcoin hasn't forked by comparing the burn hash provided - (asserts! (is-eq (some burn-hash) (get-burn-header burn-height)) ERR_INVALID_BURN_HASH) - - ;; Check that the caller is the current signer principal - (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) - - ;; Check whether it was already accepted or rejected - (asserts! (is-none (get status request)) ERR_ALREADY_PROCESSED) - - ;; Check that fee is not higher than requesters max fee - (asserts! (<= fee requested-max-fee) ERR_FEE_TOO_HIGH) - - ;; Burn the locked-sbtc - (try! (contract-call? .sbtc-token protocol-burn-locked (+ requested-amount requested-max-fee) requester withdraw-role)) - - ;; Mint the difference b/w max-fee of the request & fee actually paid back to the user in sBTC - (if (is-eq (- requested-max-fee fee) u0) - true - (try! (contract-call? .sbtc-token protocol-mint (- requested-max-fee fee) requester withdraw-role)) - ) - - ;; Call into registry to confirm accepted withdrawal - (try! (contract-call? .sbtc-registry complete-withdrawal-accept request-id bitcoin-txid output-index signer-bitmap fee burn-hash burn-height sweep-txid)) - - (ok true) - ) -) - -;; Reject a withdrawal request -(define-public (reject-withdrawal-request (request-id uint) (signer-bitmap uint)) - (let - ( - (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) - (withdrawal (unwrap! (contract-call? .sbtc-registry get-withdrawal-request request-id) ERR_INVALID_REQUEST)) - (requested-max-fee (get max-fee withdrawal)) - (requested-amount (get amount withdrawal)) - (requester (get sender withdrawal)) - ) - - ;; Check that the caller is the current signer principal - (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) - - ;; Check that request status is currently-pending - (asserts! (is-none (get status withdrawal)) ERR_ALREADY_PROCESSED) - - ;; Burn sbtc-locked & re-mint sbtc to original requester - (try! (contract-call? .sbtc-token protocol-unlock (+ requested-amount requested-max-fee) requester withdraw-role)) - - ;; Call into registry to confirm accepted withdrawal - (try! (contract-call? .sbtc-registry complete-withdrawal-reject request-id signer-bitmap)) - - (ok true) - ) -) -;; Complete multiple withdrawal requests -(define-public (complete-withdrawals (withdrawals (list 600 - {request-id: uint, - status: bool, - signer-bitmap: uint, - bitcoin-txid: (optional (buff 32)), - output-index: (optional uint), - fee: (optional uint), - burn-hash: (buff 32), - burn-height: uint, - sweep-txid: (optional (buff 32))}))) - (let - ( - (current-signer-data (contract-call? .sbtc-registry get-current-signer-data)) - ) - - ;; Check that the caller is the current signer principal - (asserts! (is-eq (get current-signer-principal current-signer-data) tx-sender) ERR_INVALID_CALLER) - - (fold complete-individual-withdrawal-helper withdrawals (ok u0)) - ) -) - -(define-private (complete-individual-withdrawal-helper (withdrawal - {request-id: uint, - status: bool, - signer-bitmap: uint, - bitcoin-txid: (optional (buff 32)), - output-index: (optional uint), - fee: (optional uint), - burn-hash: (buff 32), - burn-height: uint, - sweep-txid: (optional (buff 32))}) - (helper-response (response uint uint))) - (match helper-response - index - (let - ( - (current-request-id (get request-id withdrawal)) - (current-signer-bitmap (get signer-bitmap withdrawal)) - (current-bitcoin-txid (get bitcoin-txid withdrawal)) - (current-output-index (get output-index withdrawal)) - (current-fee (get fee withdrawal)) - ) - (if (get status withdrawal) - ;; accepted - (begin - (asserts! - (and (is-some current-bitcoin-txid) (is-some current-output-index) (is-some current-fee)) - (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) - (unwrap! (accept-withdrawal-request current-request-id (unwrap-panic current-bitcoin-txid) current-signer-bitmap (unwrap-panic current-output-index) (unwrap-panic current-fee) (get burn-hash withdrawal) (get burn-height withdrawal) (unwrap-panic (get sweep-txid withdrawal))) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) - ) - ;; rejected - (unwrap! (reject-withdrawal-request current-request-id current-signer-bitmap) (err (+ ERR_WITHDRAWAL_INDEX_PREFIX (+ u10 index)))) - ) - (ok (+ index u1)) - ) - err-response - (err err-response) - ) -) - -;; Validation methods - -;; Validate that a withdrawal's recipient address is well-formed. The logic -;; here follows the same rules as pox-4. -;; -;; At a high-level, the version must be a uint between 0 and 6 (inclusive), -;; and the length of the hashbytes must be 20 bytes if the version is <= 4, -;; and 32 bytes if the version is 5 or 6. -(define-read-only (validate-recipient (recipient { version: (buff 1), hashbytes: (buff 32) })) - (let - ( - (version (get version recipient)) - (hashbytes (get hashbytes recipient)) - (version-int (buff-to-uint-be version)) - ) - ;; Validate the `version` - (asserts! (<= version-int MAX_ADDRESS_VERSION) ERR_INVALID_ADDR_VERSION) - ;; Validate the length of `hashbytes` - (asserts! (if (<= version-int MAX_ADDRESS_VERSION_BUFF_20) - ;; If version is <= 4, then hashbytes must be 20 bytes - (is-eq (len hashbytes) u20) - ;; Otherwise, hashbytes must be 32 bytes - (is-eq (len hashbytes) u32)) - ERR_INVALID_ADDR_HASHBYTES) - (ok true) - ) -) - -;; Return the bitcoin header hash of the bitcoin block at the given height. -(define-read-only (get-burn-header (height uint)) - (get-burn-block-info? header-hash height) -)