Skip to content

Commit 5da33f6

Browse files
committed
first draft stacks executor contract
1 parent 5750696 commit 5da33f6

File tree

15 files changed

+4936
-0
lines changed

15 files changed

+4936
-0
lines changed

stacks/.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
tests/** linguist-vendored
2+
vitest.config.js linguist-vendored
3+
* text=lf

stacks/.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
**/settings/Mainnet.toml
3+
**/settings/Testnet.toml
4+
.cache/**
5+
history.txt
6+
7+
logs
8+
*.log
9+
npm-debug.log*
10+
coverage
11+
*.info
12+
costs-reports.json
13+
node_modules

stacks/.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
{
3+
"files.eol": "\n"
4+
}

stacks/.vscode/tasks.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
{
3+
"version": "2.0.0",
4+
"tasks": [
5+
{
6+
"label": "check contracts",
7+
"group": "test",
8+
"type": "shell",
9+
"command": "clarinet check"
10+
},
11+
{
12+
"type": "npm",
13+
"script": "test",
14+
"group": "test",
15+
"problemMatcher": [],
16+
"label": "npm test"
17+
}
18+
]
19+
}

stacks/Clarinet.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[project]
2+
name = 'stacks'
3+
description = ''
4+
authors = []
5+
telemetry = false
6+
cache_dir = './.cache'
7+
8+
[[project.requirements]]
9+
contract_id = 'SP1E0XBN9T4B10E9QMR7XMFJPMA19D77WY3KP2QKC.self-listing-helper-v3'
10+
[contracts.executor-state]
11+
path = 'contracts/executor-state.clar'
12+
clarity_version = 3
13+
epoch = 'latest'
14+
15+
[contracts.executor]
16+
path = 'contracts/executor.clar'
17+
clarity_version = 3
18+
epoch = '3.2'
19+
depends_on = ['executor-state']
20+
[repl.analysis]
21+
passes = ['check_checker']
22+
23+
[repl.analysis.check_checker]
24+
strict = false
25+
trusted_sender = false
26+
trusted_caller = false
27+
callee_filter = false
28+
29+
[repl.remote_data]
30+
enabled = false
31+
api_url = 'https://api.hiro.so'
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
;; title: executor-state
2+
;; version: 0.0.1
3+
;; summary: State contract for cross-chain executor relayer registry
4+
;; description: Simple relayer address mapping for the executor system
5+
6+
;;;; Constants
7+
8+
;; State contract errors
9+
(define-constant ERR_STATE_RELAYER_EXISTS (err u20001))
10+
11+
;;;; Data maps
12+
13+
;; Map to track relayer addresses for payments
14+
;; Universal address is keccak256(stacks-principal-as-string)
15+
(define-map relayer-to-stacks
16+
(buff 32) ;; Universal address (32-byte hash)
17+
principal ;; Stacks principal for STX payments
18+
)
19+
20+
;;;; Public functions
21+
22+
;; @desc Register a relayer's Stacks address for their universal address
23+
;; Anyone can call this to register themselves as a relayer
24+
(define-public (register-relayer (stacks-addr principal))
25+
(let ((p-as-string (principal-to-string stacks-addr))
26+
(universal-addr (keccak256 (string-ascii-to-buff p-as-string))))
27+
28+
;; Check if relayer already exists
29+
(asserts! (is-none (relayer-to-stacks-get universal-addr)) ERR_STATE_RELAYER_EXISTS)
30+
31+
;; Register the mapping
32+
(map-set relayer-to-stacks universal-addr stacks-addr)
33+
34+
;; Return the universal address for confirmation
35+
(ok universal-addr)))
36+
37+
;;;; Read-only functions
38+
39+
;; @desc Convert universal address to Stacks principal
40+
(define-read-only (universal-addr-to-principal (universal-addr (buff 32)))
41+
(relayer-to-stacks-get universal-addr))
42+
43+
;; @desc Helper function to convert string to buffer for hashing
44+
;; Matches the exact implementation from Wormhole Core
45+
(define-read-only (string-ascii-to-buff (s (string-ascii 256)))
46+
(let ((cb (unwrap-panic (to-consensus-buff? s))))
47+
;; Consensus buff format for string:
48+
;; bytes[0]: Consensus Buff Type
49+
;; bytes[1..4]: String length
50+
;; bytes[5..]: String data
51+
(unwrap-panic (slice? cb u5 (len cb)))))
52+
53+
;; principle to string conversion from https://explorer.hiro.so/txid/0xa0988bb5f2aa6179e61e7735b91f7276cf70106f05781a0c1c7dee663be5dc7c?chain=mainnet
54+
;; @desc Convert principal to string representation (C32 encoding)
55+
(define-read-only (principal-to-string (p principal))
56+
(let ((destructed (unwrap-panic (principal-destruct? p)))
57+
(checksum (unwrap-panic (slice? (sha256 (sha256 (concat (get version destructed) (get hash-bytes destructed)))) u0 u4)))
58+
(data (unwrap-panic (as-max-len? (concat (get hash-bytes destructed) checksum) u24)))
59+
(result (concat (concat "S" (unwrap-panic (element-at? C32 (buff-to-uint-be (get version destructed))))) (append-leading-0 data (trim-leading-0 (hash-bytes-to-string data))))))
60+
(match (get name destructed) n (concat (concat result ".") n) result)))
61+
62+
;; Constants and helpers for C32 encoding
63+
(define-constant C32 "0123456789ABCDEFGHJKMNPQRSTVWXYZ")
64+
(define-constant LIST_15 (list 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))
65+
(define-constant LIST_24 (list 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))
66+
(define-constant LIST_39 (concat LIST_24 LIST_15))
67+
68+
(define-read-only (c32-to-string-iter (idx int) (it { s: (string-ascii 39), r: uint }))
69+
{ s: (unwrap-panic (as-max-len? (concat (unwrap-panic (element-at? C32 (mod (get r it) u32))) (get s it)) u39)), r: (/ (get r it) u32) })
70+
71+
(define-read-only (hash-bytes-to-string (data (buff 24)))
72+
(let ((low-part (get s (fold c32-to-string-iter LIST_24 { s: "", r: (buff-to-uint-be (unwrap-panic (as-max-len? (unwrap-panic (slice? data u9 u24)) u16)))})))
73+
(high-part (get s (fold c32-to-string-iter LIST_15 { s: "", r: (buff-to-uint-be (unwrap-panic (as-max-len? (unwrap-panic (slice? data u0 u9)) u16)))}))))
74+
(unwrap-panic (as-max-len? (concat high-part low-part) u39))))
75+
76+
(define-read-only (trim-leading-0-iter (idx int) (it (string-ascii 39)))
77+
(if (is-eq (element-at? it u0) (some "0")) (unwrap-panic (slice? it u1 (len it))) it))
78+
79+
(define-read-only (trim-leading-0 (s (string-ascii 39)))
80+
(fold trim-leading-0-iter LIST_39 s))
81+
82+
(define-read-only (append-leading-0-iter (idx int) (it { hash-bytes: (buff 24), address: (string-ascii 39)}))
83+
(if (is-eq (element-at? (get hash-bytes it) u0) (some 0x00))
84+
{ hash-bytes: (unwrap-panic (slice? (get hash-bytes it) u1 (len (get hash-bytes it)))), address: (unwrap-panic (as-max-len? (concat "0" (get address it)) u39)) }
85+
it))
86+
87+
(define-read-only (append-leading-0 (hash-bytes (buff 24)) (s (string-ascii 39)))
88+
(get address (fold append-leading-0-iter LIST_24 { hash-bytes: hash-bytes, address: s })))
89+
90+
;;;; Map getters
91+
92+
(define-read-only (relayer-to-stacks-get (universal-addr (buff 32)))
93+
(map-get? relayer-to-stacks universal-addr))
94+

stacks/contracts/executor.clar

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
;; title: executor
2+
;; version:
3+
;; summary:
4+
;; description:
5+
6+
;; traits
7+
;;
8+
9+
;; token definitions
10+
;;
11+
12+
;; constants
13+
(define-constant EXECUTOR-VERSION "Executor-0.0.1")
14+
(define-constant OUR-CHAIN u1) ;; Must be manually updated before deployment. Worth doing it this way or an initialize call?
15+
16+
;; errors
17+
(define-constant ERR-QUOTE-SRC-CHAIN-MISMATCH (err u1001))
18+
(define-constant ERR-QUOTE-DST-CHAIN-MISMATCH (err u1002))
19+
(define-constant ERR-QUOTE-EXPIRED (err u1003))
20+
(define-constant ERR-UNREGISTERED-RELAYER (err u1004))
21+
(define-constant ERR-INVALID-PAYEE-ADDRESS (err u1005))
22+
(define-constant ERR-BUFFER-PARSE-ERROR (err u1006))
23+
;;
24+
25+
;; data vars
26+
;;
27+
28+
;; data maps
29+
;;
30+
31+
;; public functions
32+
(define-public (request-execution
33+
(dst-chain uint)
34+
(dst-addr (buff 32))
35+
(refund-addr principal)
36+
(signed-quote-bytes (buff 8192))
37+
(request-bytes (buff 8192))
38+
(relay-instructions (buff 8192))
39+
(payment uint)) ;; STX amount in microSTX
40+
41+
(begin
42+
;; Validate quote header
43+
(try! (validate-quote-header signed-quote-bytes dst-chain))
44+
45+
;; Extract quoter and payee info
46+
(match (extract-quote-addresses signed-quote-bytes)
47+
quote-addresses
48+
(let ((payee-universal-addr (get payee quote-addresses)))
49+
;; 1. Verify universal address is properly formatted (32 bytes, non-zero)
50+
(asserts! (is-eq (len payee-universal-addr) u32) ERR-INVALID-PAYEE-ADDRESS)
51+
(asserts! (not (is-eq payee-universal-addr 0x0000000000000000000000000000000000000000000000000000000000000000)) ERR-INVALID-PAYEE-ADDRESS)
52+
53+
;; 2. Verify relayer is registered and get principal
54+
(let ((payee-lookup-result (contract-call? .executor-state relayer-to-stacks-get payee-universal-addr)))
55+
(asserts! (is-some payee-lookup-result) ERR-UNREGISTERED-RELAYER)
56+
57+
;; 3. Extract the principal and validate it's not contract address
58+
(let ((payee-principal (unwrap-panic payee-lookup-result)))
59+
(asserts! (not (is-eq payee-principal (as-contract tx-sender))) ERR-INVALID-PAYEE-ADDRESS)
60+
61+
;; 4. Perform the payment after all validations pass
62+
(try! (stx-transfer? payment tx-sender payee-principal))
63+
64+
;; Emit event for off-chain relayers
65+
(print {
66+
event: "RequestForExecution",
67+
quoter-address: (get quoter quote-addresses),
68+
amount-paid: payment,
69+
dst-chain: dst-chain,
70+
dst-addr: dst-addr,
71+
refund-addr: refund-addr,
72+
signed-quote: signed-quote-bytes,
73+
request-bytes: request-bytes,
74+
relay-instructions: relay-instructions,
75+
block-height: stacks-block-height,
76+
tx-sender: tx-sender
77+
})
78+
79+
(ok true))))
80+
err-case
81+
ERR-BUFFER-PARSE-ERROR)))
82+
;;
83+
84+
;; read only functions
85+
;; Extract uint16 from buffer at specific offset (big-endian) using efficient slice operation
86+
(define-read-only (extract-uint16-be (data (buff 8192)) (offset uint))
87+
(let ((extracted (slice? data offset (+ offset u2))))
88+
(match extracted
89+
result (if (is-eq (len result) u2)
90+
(ok (buff-to-uint-be (unwrap-panic (as-max-len? result u2))))
91+
(err ERR-BUFFER-PARSE-ERROR))
92+
(err ERR-BUFFER-PARSE-ERROR))))
93+
94+
;; Extract uint64 from buffer at specific offset (big-endian) using efficient slice operation
95+
(define-read-only (extract-uint64-be (data (buff 8192)) (offset uint))
96+
(let ((extracted (slice? data offset (+ offset u8))))
97+
(match extracted
98+
result (if (is-eq (len result) u8)
99+
(ok (buff-to-uint-be (unwrap-panic (as-max-len? result u8))))
100+
(err ERR-BUFFER-PARSE-ERROR))
101+
(err ERR-BUFFER-PARSE-ERROR))))
102+
103+
;; Extract bytes32 from buffer at specific offset
104+
;; Returns a properly sized 32-byte buffer using efficient slice operation
105+
(define-read-only (extract-bytes32 (data (buff 8192)) (offset uint))
106+
(let ((extracted (slice? data offset (+ offset u32))))
107+
(match extracted
108+
result (if (is-eq (len result) u32)
109+
(ok (unwrap-panic (as-max-len? result u32)))
110+
(err ERR-BUFFER-PARSE-ERROR))
111+
(err ERR-BUFFER-PARSE-ERROR))))
112+
113+
;; Extract address (20 bytes) from buffer at specific offset
114+
(define-read-only (extract-address (data (buff 8192)) (offset uint))
115+
(slice? data offset (+ offset u20)))
116+
117+
;; Validate quote header
118+
(define-read-only (validate-quote-header (signed-quote-bytes (buff 8192)) (dst-chain uint))
119+
(match (extract-uint16-be signed-quote-bytes u56)
120+
quote-src-chain (match (extract-uint16-be signed-quote-bytes u58)
121+
quote-dst-chain (match (extract-uint64-be signed-quote-bytes u60)
122+
expiry-time (if (is-eq quote-src-chain OUR-CHAIN)
123+
(if (is-eq quote-dst-chain dst-chain)
124+
;; Currently comparing Unix timestamp (expiry-time) with block height (stacks-block-height)
125+
;; Correct way is likely to do: (get-stacks-block-info? time stacks-block-height) which should return Unix timestamp
126+
;; Unable to write tests for it though as of now.
127+
(if (> expiry-time stacks-block-height)
128+
(ok true)
129+
ERR-QUOTE-EXPIRED)
130+
ERR-QUOTE-DST-CHAIN-MISMATCH)
131+
ERR-QUOTE-SRC-CHAIN-MISMATCH)
132+
err3 ERR-BUFFER-PARSE-ERROR)
133+
err2 ERR-BUFFER-PARSE-ERROR)
134+
err1 ERR-BUFFER-PARSE-ERROR))
135+
136+
;; Extract quoter and payee addresses from quote
137+
(define-read-only (extract-quote-addresses (signed-quote-bytes (buff 8192)))
138+
(match (extract-address signed-quote-bytes u4)
139+
quoter-addr (match (extract-bytes32 signed-quote-bytes u24)
140+
payee-addr-32 (ok {
141+
quoter: quoter-addr,
142+
payee: payee-addr-32
143+
})
144+
err (err ERR-BUFFER-PARSE-ERROR))
145+
(err ERR-BUFFER-PARSE-ERROR)))
146+
147+
148+
;; Convert 32-byte universal address hash back to a Stacks principal
149+
;; Uses the executor-state contract's relayer registry
150+
(define-read-only (universal-addr-to-principal (universal-addr (buff 32)))
151+
(contract-call? .executor-state universal-addr-to-principal universal-addr))
152+
153+
;; Read-only functions for external access
154+
(define-read-only (get-executor-version)
155+
EXECUTOR-VERSION)
156+
157+
(define-read-only (get-our-chain)
158+
OUR-CHAIN)
159+
;;
160+
161+
;; private functions
162+
;;

0 commit comments

Comments
 (0)