Source Code

;; title: rate-limit
;; version: 1.0.0
;; summary: Rate-limiter for Withdrawals
;; description: Implements a per-token withdrawal rate-limiter for the asset-manager.
;;
;;  - `init` wires the contract to its collaborators (asset-manager,
;;    connection-trait, hub chain id, hub manager & admin address).
;;  - `verify-withdraw` is called by the asset-manager for every outbound
;;    transfer; it enforces max N tokens per second limits.
;;  - `recv-message` allows the hub chain to push configuration changes
;;    rate limits, pause state, admin keys, signer lists.  All hub
;;    messages are authenticated via the connection-trait and must be
;;    signed by the current hub admin or signer set.
;;
;;  Rate-limit state is stored in the companion `rate-limit-state` data
;;  contract so that it can be upgraded without losing configuration.

;; traits
(use-trait connection-contract .connection-trait.connection-trait)

;; ---------------------------------------------------------------------------
;; Constants action kinds used in hub messages
;; ---------------------------------------------------------------------------
(define-constant hub-admin-set-action u1)
(define-constant hub-signers-set-action u2)
(define-constant rate-limit-set-action u3)
(define-constant rate-limit-reset-action u4)
(define-constant pause-action u5)

;; ---------------------------------------------------------------------------
;; Error codes
;; ---------------------------------------------------------------------------
(define-constant err-not-asset-manager (err u4100))
(define-constant err-not-admin (err u4101))
(define-constant err-withdraw-limit-exceeded (err u4102))
(define-constant err-already-initialized (err u4103))
(define-constant err-not-connection-contract (err u4104))
(define-constant err-signature-verification-failed (err u4105))
(define-constant err-not-sent-from-hub (err u4106))
(define-constant err-not-hub-manager (err u4107))
(define-constant err-deadline-exceeded (err u4108))
(define-constant err-not-hub-admin (err u4109))
(define-constant err-not-hub-admin-signers (err u4110))
(define-constant err-paused (err u4111))
(define-constant err-already-executed (err u4112))
(define-constant err-invalid-action (err u4113))
(define-constant err-invalid-chain-id (err u4114))
(define-constant err-invalid-key-format (err u4115))
;;

;; ---------------------------------------------------------------------------
;; Storage to prevent replay of hub messages
;; ---------------------------------------------------------------------------
(define-map executed-messages uint bool)

;; ---------------------------------------------------------------------------
;; Administrative Setup
;; ---------------------------------------------------------------------------

;; @desc One-time initialization performed by the contract deployer/admin.
;;       Persists pointers to the asset-manager, connection layer, and
;;       hub-side identities.  Cannot be called twice.
;; @param connection   principal of the connection-trait contract
;; @param asset-manager principal of the asset-manager
;; @param hub-chain-id  id of the hub chain
;; @param hub-manager  100-byte address of the hub manager
;; @param hub-admin    100-byte address of the hub admin (allowed to send config msgs)
(define-public (init
    (connection principal)
    (asset-manager principal)
    (hub-chain-id uint)
    (hub-manager (buff 256))
    (hub-admin (buff 33))
  )
  (begin
    (try! (is-admin))
    (try! (is-not-initialized))
    (asserts! (> hub-chain-id u0) err-invalid-chain-id)
    (try! (contract-call? .rate-limit-state set-asset-manager-impl asset-manager))
    (try! (contract-call? .rate-limit-state set-connection-impl connection))
    (try! (contract-call? .rate-limit-state set-hub-configs hub-chain-id hub-manager))
    (try! (contract-call? .rate-limit-state set-hub-admin hub-admin))
    (ok true)
  )
)

;; ---------------------------------------------------------------------------
;; Core Rate-Limit Logic
;; ---------------------------------------------------------------------------
;; @dev Called by the asset-manager *before* each withdrawal.
;;      Checks the remaining allowance for the token and updates the
;;      spent amount.  Rejects the request if the limit is exceeded
;;      or the contract is paused.
;; @param token         principal of the token being withdrawn
;; @param amount        requested amount to withdraw
;; @return              (ok true) if allowed
(define-public (verify-withdraw
    (token principal)
    (amount uint)
    )
  (begin
    (try! (is-asset-manager))
    (asserts! (not (contract-call? .rate-limit-state is-paused)) err-paused)
    (let ((current-config (contract-call? .rate-limit-state get-rate-limit token)))
      (if (is-eq (get rate-per-second current-config) u0)
        (ok true)
        (let (
            (available (compute-available token current-config))
            (last-updated stacks-block-time)
          )
          (asserts! (>= available amount) err-withdraw-limit-exceeded)
          (contract-call? .rate-limit-state set-rate-limit token
            (get rate-per-second current-config) (- available amount)
            last-updated (get max-available current-config)
          )
        )
      )
    )
  )
)

;; ---------------------------------------------------------------------------
;; Inbound Hub Messages
;; ---------------------------------------------------------------------------

;; @dev Accepts authenticated configuration messages from the hub chain.
;;      Rejects any message that is not signed by the current hub admin
;;      or signer set, or if the message deadline has passed.
;; @param src-chain-id must equal configured hub-chain-id
;; @param src-address  must equal configured hub-manager address
;; @param conn-sn      connection sequence number (for replay protection)
;; @param payload      RLP-encoded message body
;; @param signatures   at most 10 signatures over payload
;; @param connection   connection-trait reference
(define-public (recv-message
    (src-chain-id uint)
    (src-address (buff 256))
    (conn-sn uint)
    (payload (buff 4096))
    (signatures (list 50 (buff 65)))
    (connection <connection-contract>)
  )
  (let ((current-connection-contract (contract-call? .rate-limit-state get-connection-impl)))
    ;; Ensure the contracts connection and rate-limit are correct
    (asserts! (is-eq (contract-of connection) current-connection-contract)
      err-not-connection-contract
    )

    ;; Ensure the message is sent from the hub chain and hub asset manager    
    (asserts!
      (is-eq src-chain-id (contract-call? .rate-limit-state get-hub-chain-id))
      err-not-sent-from-hub
    )
    (asserts!
      (is-eq src-address (contract-call? .rate-limit-state get-hub-manager))
      err-not-hub-manager
    )

    (try!
      (contract-call? .rate-limit-state verify-message src-chain-id src-address conn-sn
        payload signatures connection
      )
    )
    (let (
        (data (try! (decode-and-validate-payload payload connection)))
        (message (get data data))
        (kind (get kind data))
      )
      (try! (process-hub-message message kind))
      (ok true)
    )
  )
)

;; @dev Pauses the rate-limit contract, preventing further withdrawals.
;;      Can only be called by the hub admin or signer set.
(define-public (pause (payload (buff 4096)) (connection <connection-contract>))
  (let (
        (data (try! (decode-and-validate-payload payload connection)))
        (current-connection-contract (contract-call? .rate-limit-state get-connection-impl))
        (message (get data data))
        (kind (get kind data))
      )

      (asserts! (is-eq (contract-of connection) current-connection-contract)
        err-not-connection-contract
      )
      ;; Verify the signature
      (asserts! (is-eq kind pause-action) err-invalid-action)
      (try! (contract-call? .rate-limit-state pause))
      (ok true)
    )
)


;; ---------------------------------------------------------------------------
;; Read-only Guards & Helpers
;; ---------------------------------------------------------------------------

;; @dev Reverts if caller is not the stored admin.
(define-read-only (is-admin)
  (ok (asserts! (is-eq contract-caller (contract-call? .rate-limit-state get-admin))
    err-not-admin
  ))
)

;; @dev Reverts if caller is not the stored asset-manager.
(define-read-only (is-asset-manager)
  (ok (asserts!
    (is-eq contract-caller
      (contract-call? .rate-limit-state get-asset-manager-impl)
    )
    err-not-asset-manager
  ))
)

;; @dev Reverts if init has already been called.
(define-read-only (is-not-initialized)
  (ok (asserts! (is-eq (contract-call? .rate-limit-state get-hub-chain-id) u0)
    err-already-initialized
  ))
)

;; @dev Computes the remaining withdrawable amount for a token given its
;;      current balance and stored configuration.
(define-read-only (get-available
    (token principal)
    (current-config {
      rate-per-second: uint,
      available: uint,
      max-available: uint,
      last-updated: uint,
    })
  )
  (compute-available token current-config)
)

;;

;; ---------------------------------------------------------------------------
;; Private Helpers
;; ---------------------------------------------------------------------------

;; @dev Decodes the hub payload structure, and ensures the signature is valid:
;;      [data (buff), signature (buff 65)]
(define-private (decode-and-validate-payload (payload (buff 4096)) (connection <connection-contract>))
  (let (
      (rlp-list-payload (contract-call? .rlp-decode rlp-to-list payload))
      (payload-data (unwrap-panic (element-at? rlp-list-payload u0)))
      (signature (unwrap-panic (as-max-len? (unwrap-panic (element-at? rlp-list-payload u1)) u65)))
      (public-key (try! (contract-call? connection recover-public-key (keccak256 payload-data) signature)))

      (rlp-list-message (contract-call? .rlp-decode rlp-to-list payload-data))
      (kind (contract-call? .rlp-decode rlp-decode-uint rlp-list-message u0))
      (deadline (contract-call? .rlp-decode rlp-decode-uint rlp-list-message u1))
      (message-data (unwrap-panic (element-at? rlp-list-message u2)))
    )
    (asserts! (>= deadline stacks-block-time) err-deadline-exceeded)
    (asserts! (not (match (map-get? executed-messages deadline) res res false)) err-already-executed)
    (if (or (is-eq kind hub-admin-set-action) (is-eq kind hub-signers-set-action))
      (asserts! (contract-call? .rate-limit-state is-hub-admin public-key) err-not-hub-admin)
      (asserts! (contract-call? .rate-limit-state is-hub-admin-or-signer public-key) err-not-hub-admin-signers)
    )
    (map-set executed-messages deadline true)
    (ok {
      data: message-data,
      kind: kind,
      deadline: deadline,
    })
  )
)

;; @dev Calculates the currently available withdrawal allowance for a token.
;;      Takes into account:
;;        - elapsed time since last update (rate * time)
;;      Caps the result at max-available.
(define-private (compute-available
    (token principal)
    (current-config {
      rate-per-second: uint,
      available: uint,
      max-available: uint,
      last-updated: uint,
    })
  )
  (let (
      (current-time stacks-block-time)
      (time-elapsed (- current-time (get last-updated current-config)))
      (refill (* time-elapsed (get rate-per-second current-config)))
      (total-available (+ refill (get available current-config)))
      (new-available (if (> total-available (get max-available current-config))
        (get max-available current-config)
        total-available
      ))
    )
    new-available
  )
)

(define-private (is-valid-compressed-key (key (buff 4096)))
  (let ((prefix (unwrap! (element-at? key u0) false)))
    (and
      (is-eq (len key) u33)
      (or (is-eq prefix 0x02) (is-eq prefix 0x03))
    )
  )
)

(define-private (check-signers (key (buff 4096)) (signers (list 10 (buff 33))))
  (unwrap-panic (as-max-len? (append signers (unwrap-panic (as-max-len? key u33))) u10))
)

(define-private (check-valid-key (key (buff 33)) (valid bool))
  (and valid (is-valid-compressed-key key))
)

;; @dev Executes validated hub configuration messages.
(define-private (process-hub-message
  (message (buff 4096))
  (kind uint)
)
  (let (
        (data (contract-call? .rlp-decode rlp-to-list message))
       )
    (if (is-eq kind hub-admin-set-action)
        (let (
              (admin (unwrap-panic (as-max-len? (unwrap-panic (element-at? data u0)) u33)))
             )
             (print { 
            event: "HubAdminUpdated",
            hub-admin: admin,
          })
          (asserts! (is-valid-compressed-key admin) err-invalid-key-format)
          (contract-call? .rate-limit-state set-hub-admin admin)
        )

        (if (is-eq kind hub-signers-set-action)
            (let (
                  (signers (unwrap-panic (element-at? data u0)))
                  (signers-list
                    (fold check-signers (contract-call? .rlp-decode rlp-to-list signers) (list)))
                 )
                 (print { 
              event: "HubSignersUpdated",
              hub-signers: signers-list,
            })
              (asserts! (fold check-valid-key signers-list true) err-invalid-key-format)
              (contract-call?
                .rate-limit-state
                set-hub-signers
                signers-list
              )
            )

            (if (is-eq kind rate-limit-set-action)
                (let (
                      (token
                        (unwrap-panic
                          (from-consensus-buff? principal
                            (unwrap-panic (element-at? data u0))
                          )
                        )
                      )
                      (current-config
                        (contract-call?
                          .rate-limit-state
                          get-rate-limit
                          token
                        )
                      )
                      (rate
                        (contract-call?
                          .rlp-decode
                          rlp-decode-uint
                          data
                          u1
                        )
                      )
                      (max
                        (contract-call?
                          .rlp-decode
                          rlp-decode-uint
                          data
                          u2
                        )
                      )
                      (last-updated stacks-block-time)
                      (available
                        (if (is-eq (get rate-per-second current-config) u0)
                            max
                            (compute-available token current-config)
                        )
                      )
                     )
                  (contract-call?
                    .rate-limit-state
                    set-rate-limit
                    token
                    rate
                    available
                    last-updated
                    max
                  )
                )

                (if (is-eq kind rate-limit-reset-action)
                    (let (
                          (token
                            (unwrap-panic
                              (from-consensus-buff? principal
                                (unwrap-panic (element-at? data u0))
                              )
                            )
                          )
                          (current-config
                            (contract-call?
                              .rate-limit-state
                              get-rate-limit
                              token
                            )
                          )
                          (rate (get rate-per-second current-config))
                          (max-available
                            (get max-available current-config))
                          (last-updated stacks-block-time)
                          (available max-available)
                         )
                       (print {
                  event: "RateLimitReset",
                  token: token
                })
                      (contract-call?
                        .rate-limit-state
                        set-rate-limit
                        token
                        rate
                        available
                        last-updated
                        max-available
                      )
                    )

                    (if (is-eq kind pause-action)
                        (contract-call? .rate-limit-state pause)
                        err-invalid-action
                    )
                )
            )
        )
    )
  )
)

Functions (11)

FunctionAccessArgs
initpublicconnection: principal, asset-manager: principal, hub-chain-id: uint, hub-manager: (buff 256
recv-messagepublicsrc-chain-id: uint, src-address: (buff 256
pausepublicpayload: (buff 4096
is-adminread-only
is-asset-managerread-only
is-not-initializedread-only
decode-and-validate-payloadprivatepayload: (buff 4096
is-valid-compressed-keyprivatekey: (buff 4096
check-signersprivatekey: (buff 4096
check-valid-keyprivatekey: (buff 33
process-hub-messageprivatemessage: (buff 4096