Skip to content

Commit

Permalink
Merge pull request #17 from hirosystems/release/milestone-2
Browse files Browse the repository at this point in the history
test: increase coverage
  • Loading branch information
Ludo Galabru authored Oct 20, 2023
2 parents 2774c23 + 01149e4 commit eeea91d
Show file tree
Hide file tree
Showing 26 changed files with 3,653 additions and 1,137 deletions.
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"trailingComma": "all"
}
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,25 @@ Price feeds are available on multiple blockchains and can be used in off-chain a

[Wormhole](https://wormhole.com) is a decentralized attestation engine that leverages its network of guardians to trustlessly bridge information between the chains it supports. Wormhole has a simple, elegant, and pragmatic design that has enabled it to be the first real solution to ship to market and has received wide recognition and support from its member chains.

## Setup and Test a Devnet Bridge

The bridge is being operated through an off-chain service, `stacks-pyth-relayer`, and a set of contracts implementing the core functionalities specified by the Wormhole protocol.
## Setup and and run the tests

The contracts are developed in Clarity and use Clarinet for its test harnessing.
This guide assumes that a recent installation of Clarinet (available on brew and winget) is available locally.
The contracts are developed in Clarity and use [clarinet-sdk](https://www.npmjs.com/package/@hirosystems/clarinet-sdk) for its test harnessing.

Git clone and compile **stacks-pyth-relayer**

```bash
$ git clone https://github.com/hirosystems/stacks-pyth-bridge.git
$ cd stacks-pyth-bridge
$ npm install
$ npm test
```

## Setup and Test a Devnet Bridge

This guide assumes that a recent installation of Clarinet (available on brew and winget) is available locally.

The bridge can be operated through an off-chain service, `stacks-pyth-relayer`, and a set of contracts implementing the core functionalities specified by the Wormhole protocol.

Start a local Devnet using the command:
```bash
$ clarinet integrate
Expand Down Expand Up @@ -183,8 +188,6 @@ These events can be observed using [Chainhook](https://github.com/hirosystems/ch

## Todos:

- [ ] Resolve todos, mostly aiming at adding more checks in both wormhole and pyth contracts
- [ ] Address insufficient test coverage
- [ ] Resolve build warnings
- [ ] Improve documentation
- [ ] Add example
- [ ] Resolve remaining todo
- [ ] Document usage
- [ ] Document example/cbtc
12 changes: 6 additions & 6 deletions contracts/hiro-kit/hk-cursor-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@
{ bytes: (get bytes cursor), pos: (+ (get pos cursor) offset) })

(define-read-only (slice (cursor { bytes: (buff 8192), pos: uint }) (size (optional uint)))
(unwrap-panic (slice?
(get bytes cursor)
(get pos cursor)
(match size value
(+ (get pos cursor) value)
(len (get bytes cursor))))))
(match (slice? (get bytes cursor)
(get pos cursor)
(match size value
(+ (get pos cursor) value)
(len (get bytes cursor))))
bytes bytes 0x))
19 changes: 11 additions & 8 deletions contracts/pyth-governance-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
(define-constant GOVERNANCE_UPGRADE_CONTRACT_WORMHOLE_CORE 0x06)
;; Fee is charged when you submit a new price
(define-constant GOVERNANCE_SET_RECIPIENT_ADDRESS 0xa0)
;; Error unauthorized control flow
(define-constant ERR_UNAUTHORIZED_ACCESS (err u404))


(define-data-var fee-value
{ mantissa: uint, exponent: uint }
Expand Down Expand Up @@ -72,24 +75,24 @@
;; Other contract
(if (is-eq contract-caller (get pyth-decoder-contract expected-execution-plan))
;; The decoding contract is checking its execution flow
(let ((execution-plan (unwrap! execution-plan-opt (err u10))))
(let ((execution-plan (unwrap! execution-plan-opt ERR_UNAUTHORIZED_ACCESS)))
;; Must always be invoked by the proxy
(try! (expect-contract-call-performed-by-expected-oracle-contract former-contract-caller expected-execution-plan))
;; Ensure that wormhole contract is the one expected
(try! (expect-active-wormhole-contract (get wormhole-core-contract execution-plan) expected-execution-plan)))
(if (is-eq contract-caller (get pyth-oracle-contract expected-execution-plan))
;; The proxy contract is checking its execution flow
(let ((execution-plan (unwrap! execution-plan-opt (err u10))))
(let ((execution-plan (unwrap! execution-plan-opt ERR_UNAUTHORIZED_ACCESS)))
;; This contract must always be invoked by the proxy
(try! (expect-contract-call-performed-by-expected-oracle-contract former-contract-caller expected-execution-plan))
;; (try! (expect-contract-call-performed-by-expected-oracle-contract former-contract-caller expected-execution-plan))
;; Ensure that storage contract is the one expected
(try! (expect-active-storage-contract (get pyth-storage-contract execution-plan) expected-execution-plan))
;; Ensure that decoder contract is the one expected
(try! (expect-active-decoder-contract (get pyth-decoder-contract execution-plan) expected-execution-plan))
;; Ensure that wormhole contract is the one expected
(try! (expect-active-wormhole-contract (get wormhole-core-contract execution-plan) expected-execution-plan)))
false)))))
(if success (ok true) (err u0))))
(if success (ok true) ERR_UNAUTHORIZED_ACCESS)))

(define-read-only (check-storage-contract
(storage-contract <pyth-storage-trait>))
Expand All @@ -108,7 +111,7 @@
(begin
(asserts!
(is-eq former-contract-caller (get pyth-oracle-contract expected-plan))
(err u0))
ERR_UNAUTHORIZED_ACCESS)
(ok true)))

(define-private (expect-active-storage-contract
Expand All @@ -123,7 +126,7 @@
(asserts!
(is-eq
(contract-of storage-contract)
(get pyth-storage-contract expected-plan)) (err u1))
(get pyth-storage-contract expected-plan)) ERR_UNAUTHORIZED_ACCESS)
(ok true)))

(define-private (expect-active-decoder-contract
Expand All @@ -138,7 +141,7 @@
(asserts!
(is-eq
(contract-of decoder-contract)
(get pyth-decoder-contract expected-plan)) (err u2))
(get pyth-decoder-contract expected-plan)) ERR_UNAUTHORIZED_ACCESS)
(ok true)))

(define-private (expect-active-wormhole-contract
Expand All @@ -153,7 +156,7 @@
(asserts!
(is-eq
(contract-of wormhole-contract)
(get wormhole-core-contract expected-plan)) (err u3))
(get wormhole-core-contract expected-plan)) ERR_UNAUTHORIZED_ACCESS)
(ok true)))

(define-read-only (get-current-execution-plan)
Expand Down
7 changes: 4 additions & 3 deletions contracts/pyth-oracle-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@
(pyth-storage-contract (get pyth-storage-contract execution-plan))
(prices-updates (try! (contract-call? pyth-decoder-contract decode-and-verify-price-feeds price-feed-bytes wormhole-core-contract)))
(fee-info (contract-call? .pyth-governance-v1 get-fee-info))
(fee-amount (* (get mantissa fee-info) (pow u10 (get exponent fee-info)))))
(fee-amount (+ u1 ;; Dust fee
(* (len prices-updates) (* (get mantissa fee-info) (pow u10 (get exponent fee-info)))))))
;; Charge fee
(unwrap! (stx-transfer? (* (len prices-updates) fee-amount) tx-sender (get address fee-info)) (err u0))
(unwrap! (stx-transfer? fee-amount tx-sender (get address fee-info)) ERR_BALANCE_INSUFFICIENT)
;; Update storage
(try! (contract-call? pyth-storage-contract write-batch prices-updates))
(try! (contract-call? pyth-storage-contract write prices-updates))
(ok prices-updates))))
21 changes: 13 additions & 8 deletions contracts/pyth-pnau-decoder-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
;; Price Feeds Ids (https://pyth.network/developers/price-feed-ids#pyth-evm-mainnet)
(define-constant STX_USD 0xec7a775f46379b5e943c3526b1c8d54cd49749176b0b98e02dde68d1bd335c17)
(define-constant BTC_USD 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
(define-constant ETH_USD 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace)

(define-constant PNAU_MAGIC 0x504e4155) ;; 'PNAU': Pyth Network Accumulator Update
(define-constant AUWV_MAGIC 0x41555756) ;; 'AUWV': Accumulator Update Wormhole Verficiation
Expand All @@ -35,12 +34,14 @@
(define-constant MERKLE_ROOT_MISMATCH (err u2006))
;; Price not found
(define-constant ERR_NOT_FOUND (err u0))
;; Price not found
(define-constant ERR_UNAUTHORIZED_FLOW (err u2404))

;;;; Public functions
(define-public (decode-and-verify-price-feeds (pnau-bytes (buff 8192)) (wormhole-core-address <wormhole-core-trait>))
(begin
;; Ensure that updates are always coming from the proxy contract
(asserts! (is-eq contract-caller .pyth-proxy-v1) (err u1))
;; Ensure that updates are always coming from the oracle contract
(asserts! (is-eq contract-caller .pyth-oracle-v1) ERR_UNAUTHORIZED_FLOW)
;; Proceed to update
(let ((prices-updates (try! (decode-pnau-price-update pnau-bytes wormhole-core-address))))
(ok prices-updates))))
Expand All @@ -58,8 +59,6 @@
(contract-call? .hk-cursor-v1 slice (get next cursor-pnau-vaa) none)
(get merkle-root-hash (get value cursor-merkle-root-data)))))
(prices-updates (map cast-decoded-price decoded-prices-updates)))
;; (watched-prices-feeds (var-get watched-price-feeds))
;; (updated-prices-feeds (get updated-prices-feeds (fold process-prices-attestations-batch decoded-prices-attestations-batches { input: watched-prices-feeds, updated-prices-feeds: (list) }))))
(ok prices-updates)))

(define-private (parse-merkle-root-data-from-vaa-payload (payload-vaa-bytes (buff 8192)))
Expand All @@ -76,7 +75,7 @@
;; Check payload type
(asserts! (is-eq (get value cursor-payload-type) AUWV_MAGIC) ERR_MAGIC_BYTES)
;; Check update type
(asserts! (is-eq (get value cursor-wh-update-type) u0) (err u999))
(asserts! (is-eq (get value cursor-wh-update-type) u0) ERR_PROOF_TYPE)
(ok {
value: {
merkle-root-slot: (get value cursor-merkle-root-slot),
Expand Down Expand Up @@ -205,7 +204,7 @@
(cursor-ema-conf (unwrap-panic (contract-call? .hk-cursor-v1 read-uint-64 (get next cursor-ema-price))))
(cursor-proof (contract-call? .hk-cursor-v1 advance (get next cursor-message-size) (get value cursor-message-size)))
(cursor-proof-size (unwrap-panic (contract-call? .hk-cursor-v1 read-uint-8 cursor-proof)))
(proof-bytes (contract-call? .hk-cursor-v1 slice (get next cursor-proof-size) none))
(proof-bytes (contract-call? .hk-cursor-v1 slice (get next cursor-proof-size) (some (* u20 (get value cursor-proof-size)))))
(leaf-bytes (contract-call? .hk-cursor-v1 slice (get next cursor-message-size) (some (get value cursor-message-size))))
(proof (get result (fold parse-proof proof-bytes {
result: (list),
Expand All @@ -219,7 +218,13 @@
{
cursor: {
index: (+ (get index (get cursor acc)) u1),
next-update-index: (+ (get index (get cursor acc)) (+ (get pos (get next cursor-proof-size)) (get value cursor-proof-size))),
next-update-index:
(+
(get index (get cursor acc))
u2
(get value cursor-message-size)
u1
(* (get value cursor-proof-size) u20)),
},
bytes: (get bytes acc),
result: (unwrap-panic (as-max-len? (append (get result acc) {
Expand Down
60 changes: 46 additions & 14 deletions contracts/pyth-store-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,13 @@
prev-publish-time: uint,
})

(define-data-var timestamps uint u0)
(define-map timestamps (buff 32) uint)

(define-public (read (price-identifier (buff 32)))
(let ((entry (unwrap! (map-get? prices price-identifier) (err u404))))
(ok entry)))

(define-public (write (price-identifier (buff 32)) (data {
price: int,
conf: uint,
expo: int,
ema-price: int,
ema-conf: uint,
publish-time: uint,
prev-publish-time: uint,
}))
(ok u1))

(define-public (write-batch (batch (list 64 {
(define-public (write (batch-updates (list 64 {
price-identifier: (buff 32),
price: int,
conf: uint,
Expand All @@ -42,4 +31,47 @@
publish-time: uint,
prev-publish-time: uint,
})))
(ok u1))
(begin
;; Ensure that updates are always coming from the right contract
(try! (contract-call? .pyth-governance-v1 check-execution-flow contract-caller none))
;; Update storage, count the number of updates
(ok (fold + (map write-batch-entry batch-updates) u0))))

(define-private (write-batch-entry (entry {
price-identifier: (buff 32),
price: int,
conf: uint,
expo: int,
ema-price: int,
ema-conf: uint,
publish-time: uint,
prev-publish-time: uint,
}))
(if (write-update (get price-identifier entry) {
price: (get price entry),
conf: (get conf entry),
expo: (get expo entry),
ema-price: (get ema-price entry),
ema-conf: (get ema-conf entry),
publish-time: (get publish-time entry),
prev-publish-time: (get prev-publish-time entry)
})
u1
u0))

(define-private (write-update (price-identifier (buff 32)) (data {
price: int,
conf: uint,
expo: int,
ema-price: int,
ema-conf: uint,
publish-time: uint,
prev-publish-time: uint,
}))
(begin
(if (not (is-price-update-outdated price-identifier (get publish-time data)))
(map-set prices price-identifier data)
false)))

(define-private (is-price-update-outdated (price-identifier (buff 32)) (publish-time uint))
(< publish-time (default-to u0 (map-get? timestamps price-identifier))))
13 changes: 1 addition & 12 deletions contracts/pyth-traits-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,7 @@
prev-publish-time: uint,
} uint))

(write ((buff 32) {
price: int,
conf: uint,
expo: int,
ema-price: int,
ema-conf: uint,
publish-time: uint,
prev-publish-time: uint,
}) (response uint uint))

(write-batch ((list 64 {
(write ((list 64 {
price-identifier: (buff 32),
price: int,
conf: uint,
Expand All @@ -52,7 +42,6 @@
publish-time: uint,
prev-publish-time: uint,
})) (response uint uint))

)
)

Expand Down
Loading

0 comments on commit eeea91d

Please sign in to comment.