Skip to content

Commit f1d9a5f

Browse files
werner-stacksgitbook-bot
authored andcommitted
GITBOOK: Add Deploy a Signer Manager Contract page
1 parent 90ab75d commit f1d9a5f

2 files changed

Lines changed: 242 additions & 0 deletions

File tree

‎docs/operate/SUMMARY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* [How to Monitor Signer](run-a-signer/how-to-monitor-signer.md)
2323
* [Best Practices to Run a Signer](run-a-signer/best-practices-to-run-a-signer.md)
2424
* [OpSec Best Practices](run-a-signer/opsec-best-practices.md)
25+
* [Deploy a Signer Manager Contract](deploy-a-signer-manager-contract.md)
2526
* [Run a sBTC Signer](run-a-sbtc-signer/README.md)
2627
* [Best Practices for Running a sBTC Signer](run-a-sbtc-signer/best-practices-for-running-an-sbtc-signer.md)
2728
* [Snapshot the Chainstate](snapshot-the-chainstate.md)
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
description: >-
3+
For signer-key holders and pool operators deploying and binding a
4+
signer-manager contract under PoX-5.
5+
---
6+
7+
# Deploy a Signer Manager Contract
8+
9+
{% hint style="info" %}
10+
All Clarity references on this page are pinned to [`pox-5.clar` at `stacks-core` 4.0.1](https://github.com/stacks-network/stacks-core/blob/4.0.1/stackslib/src/chainstate/stacks/boot/pox-5.clar) and to a [pinned mainnet build of the reference signer-manager](https://github.com/stx-labs/signer-sidekick/blob/214c67eae2f1ce1c3c818ab6528ce4f2e1bdc22a/contracts/reference-manager/generated/mainnet/signer-manager.clar).
11+
{% endhint %}
12+
13+
### Who needs this page
14+
15+
Most integrators never write a signer-manager — they pick a signer and route through it, or they use the reference implementation as-is. This page is for the signer-key holder and operator side: whoever deploys a signer-manager contract and binds it to a signer key so stakers can route through it.
16+
17+
### What a signer is in PoX-5
18+
19+
* A signer is no longer a `{ btcRewardAddress, signerKey }` tuple in a reward set — it is a **signer-manager contract**, identified in all contract state by its principal, with one associated signer key.
20+
* Every staker (STX-only or bond participant) delegates to a signer-manager. There is no separate "solo" signer set — a solo staker is just a signer-manager with one member.
21+
* The pox-5 contract hooks into the manager during staking actions via `validate-stake!`, replacing PoX-4's per-transaction signer signatures that scoped each call by amount, period, and pox-addr.
22+
* Most operators do not need to write a contract at all. The reference signer-manager covers the common case: it accepts delegations, registers the signer key once, and handles reward accounting and onward distribution. Write your own only for custom needs — allowlisting members, different fee logic, custom sBTC routing.
23+
24+
{% hint style="warning" %}
25+
**"Immutable" applies to the code, not the terms.** A deployed contract's Clarity source can never change, but the reference manager's _parameters_ can. An admin can call `update-fees` at any time, up to 99.99%, and `update-admin` lets any existing admin add another. Stakers should read a manager's admin set and current fee, not assume its economics are fixed.
26+
{% endhint %}
27+
28+
### The `signer-manager` trait
29+
30+
Your contract must implement one method:
31+
32+
```clarity
33+
(define-trait signer-manager-trait (
34+
(validate-stake!
35+
;; staker, first-index, num-indexes, amount-ustx, amount-sats, is-bond, signer-calldata
36+
(principal uint uint uint uint bool (optional (buff 500)))
37+
(response bool uint)
38+
)
39+
))
40+
```
41+
42+
* `first-index` is a reward cycle for STX-only flows and a bond index for bond flows.
43+
* `num-indexes` is the cycle count for STX-only, or `u1` for a bond.
44+
* `amount-sats` is `u0` for STX-only, or the BTC commitment for bond legs.
45+
* It is invoked only through pox-5's internal `signer-manager-validate-stake` wrapper, which raises a reentrancy guard (`signer-manager-call-active`) for the duration of the call. Any attempt to re-enter pox-5 from inside your `validate-stake!` callback fails with `ERR_REENTRANT_CALL (u49)`.
46+
* pox-5 settles per-staker rewards itself, so your contract does not need to track staker-level accounting.
47+
48+
Your contract must also gate `validate-stake!` so only pox-5 can call it. The reference manager does this with:
49+
50+
```clarity
51+
(define-private (authorize-pox-5)
52+
(ok (asserts! (is-eq contract-caller 'SP000000000000000000002Q6VF78.pox-5)
53+
ERR_UNAUTHORIZED_CALLER
54+
))
55+
)
56+
```
57+
58+
### Deploy your contract
59+
60+
Deploy a Clarity contract implementing `signer-manager-trait`. The [pinned mainnet reference manager](https://github.com/stx-labs/signer-sidekick/blob/214c67eae2f1ce1c3c818ab6528ce4f2e1bdc22a/contracts/reference-manager/generated/mainnet/signer-manager.clar) is the recommended starting point — it is generated for mainnet, so its pox-5 and sBTC principals are already correct.
61+
62+
{% hint style="danger" %}
63+
Do not deploy the `stacks-core` test fixture under `contrib/core-contract-tests/`. Its pox-5 and sBTC principals do not match mainnet. Verify the canonical hash of whatever source you deploy against the expected value before broadcasting, and confirm the deployed principal on-chain afterwards — a transaction ID confirms submission, not success.
64+
{% endhint %}
65+
66+
The reference manager's deployer becomes its first admin automatically (`(map-set admins tx-sender true)`). Rotate admin control to a cold wallet before accepting stake, and never remove the last working admin.
67+
68+
{% hint style="warning" %}
69+
Ledger Stacks App versions through `0.26.17` cannot sign the Clarity 6 deployment payload. Deploy with a temporary software wallet, then rotate admin control to the Ledger or cold wallet before registration or staking.
70+
{% endhint %}
71+
72+
### Register the signer key — `register-signer`
73+
74+
After deployment, the signer-manager contract itself calls `pox-5::register-signer` to bind itself to a signer key.
75+
76+
```clarity
77+
;; Only the signer contract itself can register itself
78+
(asserts! (is-eq contract-caller signer)
79+
ERR_UNAUTHORIZED_SIGNER_REGISTRATION
80+
)
81+
```
82+
83+
`contract-caller` must equal the signer-manager. It cannot be forwarded through an intermediary contract, and it cannot be sent by an EOA on the manager's behalf. Violating this aborts with `ERR_UNAUTHORIZED_SIGNER_REGISTRATION (u26)`.
84+
85+
Registration also re-checks the grant first, so the grant must already be in place — see the next section. Read the registered key back on-chain with `get-signer-info`, or via `fetchSignerInfo` in the SDK.
86+
87+
### Grant the signer-manager permission — `grant-signer-key`
88+
89+
{% stepper %}
90+
{% step %}
91+
#### Build the SIP-018 grant message
92+
93+
`get-signer-grant-message-hash` hashes a slim tuple under a domain pinned to pox-5:
94+
95+
```clarity
96+
(define-constant POX_5_SIGNER_DOMAIN {
97+
name: "pox-5-signer",
98+
version: "1.0.0",
99+
chain-id: chain-id,
100+
})
101+
102+
;; hashed alongside:
103+
{
104+
topic: "grant-authorization",
105+
signer-manager: signer-manager,
106+
auth-id: auth-id,
107+
}
108+
```
109+
110+
The grant binds a signer key to a signer-manager and **nothing else** — no `max-amount`, `period`, `reward-cycle`, or `pox-addr`. The on-chain grant map is keyed only on `{ signer-key, signer-manager }`; replay protection adds `auth-id` in a separate map. This is the substantive break from PoX-4's per-transaction scoped signatures.
111+
{% endstep %}
112+
113+
{% step %}
114+
#### Sign it off-chain, then submit from the manager
115+
116+
The signer-key holder signs the hash off-chain with `signSignerGrant`. The **signer-manager contract** then submits `grant-signer-key`, which enforces the same caller rule as registration:
117+
118+
```clarity
119+
;; Only the signer contract itself can call this function to grant a signer key
120+
(asserts! (is-eq contract-caller signer-manager)
121+
ERR_UNAUTHORIZED_SIGNER_REGISTRATION
122+
)
123+
```
124+
125+
The reference manager wraps both calls in a single `register-self` entrypoint, submitting `grant-signer-key` and then `register-signer`.
126+
{% endstep %}
127+
128+
{% step %}
129+
#### Verify before submitting
130+
131+
* `fetchEligibleGrantSignerKey` — preflight, returns the failure reasons the call would hit
132+
* `fetchSignerGrantMessageHash` — cross-check the hash against the chain
133+
* `fetchVerifySignerKeyGrant` — is there already an active grant for this pair
134+
* `fetchSignerKeyGrantUsed` — replay-guard check for a given `authId`
135+
{% endstep %}
136+
{% endstepper %}
137+
138+
```typescript
139+
import {
140+
buildGrantSignerKey,
141+
fetchEligibleGrantSignerKey,
142+
signSignerGrant,
143+
} from '@stacks/bitcoin-staking';
144+
145+
const signerSignature = signSignerGrant({
146+
signerManager,
147+
authId,
148+
chainId,
149+
privateKey: signer.privateKey,
150+
});
151+
152+
const eligible = await fetchEligibleGrantSignerKey({
153+
signerKey: signer.publicKey,
154+
signerManager,
155+
authId,
156+
signerSignature,
157+
network,
158+
});
159+
if (!eligible.ok) {
160+
throw new Error('grant-signer-key would fail: u' + eligible.reasons.join(', u'));
161+
}
162+
163+
const tx = await buildGrantSignerKey({
164+
signerKey: signer.publicKey,
165+
signerManager,
166+
authId,
167+
publicKey: user.stxPublicKey,
168+
signerSignature,
169+
fee: 10_000n,
170+
nonce,
171+
network,
172+
});
173+
```
174+
175+
{% hint style="danger" %}
176+
Generate the signature on the signer host. Never paste a signer private key or seed phrase into a website, chat, or email.
177+
{% endhint %}
178+
179+
{% hint style="info" %}
180+
Builders in `@stacks/bitcoin-staking` attach no post-conditions and default to `Deny` mode. Any transaction you build that moves an asset needs explicit post-conditions.
181+
{% endhint %}
182+
183+
### Revoke a grant — `revoke-signer-grant`
184+
185+
Takes `signer-manager` first, then `signer-key`. It must be sent **directly** by the Stacks principal derived from the signer key — `contract-caller` is the authorization, so it cannot be forwarded through an intermediary contract, and no SIP-018 message is needed:
186+
187+
```clarity
188+
(asserts!
189+
(is-eq
190+
(unwrap-panic (principal-construct?
191+
(if is-in-mainnet
192+
STACKS_ADDR_VERSION_MAINNET
193+
STACKS_ADDR_VERSION_TESTNET
194+
)
195+
(hash160 signer-key)
196+
))
197+
contract-caller
198+
)
199+
ERR_UNAUTHORIZED
200+
)
201+
```
202+
203+
Calling from any other principal fails with `ERR_UNAUTHORIZED (u1)`.
204+
205+
Revoking is not a kill switch. It blocks future `register-signer` calls for that pair, and because every new-stake entrypoint re-checks the grant, it also stops an already-registered manager accepting **new** stake. Existing positions are left alone: the manager's `signers` entry stays intact so outstanding obligations can still be settled, and those positions wind down as their bonds and stakes expire.
206+
207+
### Error codes
208+
209+
From pox-5:
210+
211+
| Code | Constant | Meaning |
212+
| ----- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
213+
| `u1` | `ERR_UNAUTHORIZED` | `revoke-signer-grant` sent by a principal other than the one derived from the signer key. |
214+
| `u12` | `ERR_SIGNER_KEY_GRANT_USED` | Replay protection — `(signer-key, signer-manager, auth-id)` already used. |
215+
| `u13` | `ERR_INVALID_SIGNATURE_RECOVER` | Could not recover a public key from the 65-byte RSV signature. |
216+
| `u14` | `ERR_INVALID_SIGNATURE_PUBKEY` | Recovered pubkey does not match the `signer-key` argument. |
217+
| `u17` | `ERR_SIGNER_KEY_GRANT_NOT_FOUND` | A staking or registration entrypoint ran against a `(signer-manager, signer-key)` pair with no active grant. |
218+
| `u26` | `ERR_UNAUTHORIZED_SIGNER_REGISTRATION` | `register-signer` or `grant-signer-key` invoked with `contract-caller != signer-manager`. |
219+
| `u49` | `ERR_REENTRANT_CALL` | Reentrant call into pox-5 while a `signer-manager-validate-stake` call was in flight. |
220+
221+
The reference signer-manager has its own separate error namespace, which you will hit during setup rather than staking:
222+
223+
| Code | Constant | Meaning |
224+
| ------- | ------------------------- | ------------------------------------------------------------------- |
225+
| `u1002` | `ERR_UNAUTHORIZED_ADMIN` | An admin-only call from a non-admin, or proxied through a contract. |
226+
| `u1005` | `ERR_INVALID_FEES_BIPS` | Fee outside the allowed range. |
227+
| `u1006` | `ERR_UNAUTHORIZED_CALLER` | `validate-stake!` invoked by something other than pox-5. |
228+
| `u1007` | `ERR_INSUFFICIENT_FEES` | Attempted to withdraw more fees than have accrued. |
229+
230+
### Signer fees
231+
232+
A signer-manager may take a commission on the rewards it distributes. This is contract-level logic, not a pox-5 protocol feature — pox-5 settles per-staker rewards to the manager, and the manager decides whether to retain a fee before onward distribution.
233+
234+
In the reference manager:
235+
236+
* `update-fees` sets the fee, in basis points. Admin-only. The default at deploy is `u0`, and the bound is one-sided — `(asserts! (< new-fees MAX_BIPS) ERR_INVALID_FEES_BIPS)` with `MAX_BIPS` of `u10000`, so any value from `u0` to `u9999` is accepted, i.e. 0% to 99.99%.
237+
* `withdraw-fees` collects accrued fees, taking an amount and a recipient. Admin-only, and capped at what has actually accrued.
238+
239+
Both require a direct call from an admin EOA — `authorize-admin` asserts `(is-eq contract-caller tx-sender)`, so admin actions cannot be proxied through another contract.
240+
241+
The fee is a percentage of sBTC rewards, deducted before payout, including before an sBTC-to-L1 BTC withdrawal. It applies to both bond participants and STX-only stakers. A dedicated **Take a signer fee** guide covers the operator workflow in detail.

0 commit comments

Comments
 (0)