Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: increase coverage #17

Merged
merged 18 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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