;; 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
)
)
)
)
)
)
)