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

Implement Asset Manager #4

Open
CyrusVorwald opened this issue Aug 4, 2024 · 0 comments
Open

Implement Asset Manager #4

CyrusVorwald opened this issue Aug 4, 2024 · 0 comments
Labels
enhancement New feature or request

Comments

@CyrusVorwald
Copy link
Contributor

Overview

The Asset Manager handles the custody and transfer of assets across different chains using a hub-and-spoke model. The main chain (hub) contains the central Asset Manager, while each integrated blockchain (spoke) has its own Asset Manager implementation. In this case, ICON is the hub and Stacks is the spoke.

Example Usage

Deposit (from Stacks to ICON):

Alice initiates a transfer of 100 STX to Bob's address on ICON.

The Stacks Asset Manager receives Alice's request, locks 100 STX in its contract, sends an RLP-encoded Deposit message to Stacks xCall, and prepares a DepositRevert message for potential rollback.

Deposit message sent to Stacks xCall contract:

{
  method: "Deposit",
  tokenAddress: "'stacks.nid/stx",
  fromAddress: "stacks.nid/alice",
  toAddress: "icon.nid/bob",
  amount: 100,
  data: ""
}

DepositRevert message held for potential rollback (not sent):

{
  method: "DepositRevert",
  tokenAddress: "stacks.nid/stx",
  toAddress: "stacks.nid/alice",
  amount: 100
}

If Bob fails to receive 100 STX on ICON, the failure is communicated back through the xCall system. Stacks Asset Manager would receive a failure notification and then process the DepositRevert message, unlocking the 100 STX and returning them to Alice's address.

Withdrawal (from ICON to Stacks):

Bob initiates a withdrawal of 50 STX on ICON, specifying Alice's Stacks address as the recipient.

The ICON Asset Manager receives Bob's request, burns 50 STX from Bob's account, sends an RLP-encoded WithdrawTo message to ICON xCall, and prepares a The Stacks Asset Manager receives this RLP-encoded message from the Stacks xCall contract and decodes it.

WithdrawTo message received from Stacks xCall contract and decoded:

{
  method: "WithdrawTo",
  tokenAddress: "'icon.nid/stx",
  toAddress: "stacks.nid/alice",
  amount: 50
}

If Alice fails to receive 50 STX on Stacks, the failure is communicated back through the xCall system. The ICON Asset Manager would receive a failure notification and then process the WithdrawRollback, minting 50 STX to Bob's account.

WithdrawRollback message sent to ICON xCall contract:

{
  method: "WithdrawRollback",
  tokenAddress: "'icon.nid/stx",
  toAddress: "icon.nid/bob",
  amount: 50
}

Requirements

Cross-Chain Message Handling

Cross-chain Network Address Representation

Implement cross-chain address representation.

Message Serialization

Implement RLP encoding/decoding for the following cross-chain messages:

;; Asset Deposit Message
;; For receiving assets from other chains
(define-data-var deposit-message
(tuple
  (method (string-ascii 7))        ;; "Deposit"
  (token-address (string-ascii 42))
  (from-address (string-ascii 42))
  (to-address (string-ascii 42))
  (amount uint)
  (data (optional (buff 256)))
)
)

;; Asset Deposit Revert Message
;; For handling failed deposits
(define-data-var deposit-revert-message
(tuple
  (method (string-ascii 13))       ;; "DepositRevert"
  (token-address (string-ascii 42))
  (to-address (string-ascii 42))
  (amount uint)
)
)

;; Token Asset Withdrawal Message
;; For sending assets to other chains
(define-data-var withdraw-message
(tuple
  (method (string-ascii 8))        ;; "Withdraw"
  (token-address (string-ascii 42))
  (to-address (string-ascii 42))
  (amount uint)
)
)

;; Native Asset Withdrawal Message
;; For sending native STX to other chains
(define-data-var withdraw-native-message
(tuple
  (method (string-ascii 14))       ;; "WithdrawNative"
  (to-address (string-ascii 42))
  (amount uint)
)
)
handle-call-message

Implement a handle-call-message function from the CallServiceReceiver trait to process incoming cross-chain messages. This function should:

  • Verify the sender (only accept from trusted cross-chain protocol)
  • Decode the incoming message
  • Route to appropriate handling logic based on message type
;; CallServiceReceiver Trait
(define-trait call-service-receiver-trait
(
  ;; Handle the call message received from the source chain
  ;; Only called from the Call Message Service
  (handle-call-message ((string-ascii 150) (buff 1024) (list 50 (string-ascii 150))) (response bool uint))
)
)
;; Function: handle-call-message
;; Description: Handles incoming cross-chain messages
;; Parameters:
;;   - from: (string-ascii 64) - The source address (usually the main Asset Manager)
;;   - data: (buff 1024) - The message data
;;   - protocols: (list 10 (string-ascii 64)) - List of protocols used for the message
;; Returns: (response bool uint) - OK with true if successful, ERR with error code if failed
(define-public (handle-call-message (from (string-ascii 64)) (data (buff 1024)) (protocols (list 10 (string-ascii 64))))
(begin
  ;; Implementation
  ;; 1. Verify the message source and protocols
  ;; 2. Decode the message data
  ;; 3. Route to the appropriate function (deposit, deposit-native, or custom actions)
  (ok true)
)
)

NOTE: In Clarity, when passed as a parameter or stored, a contract principal becomes a non-callable value that can only be used for identification. We can't dynamically dispatch to a contract based on a runtime value nor add new trait implementations dynamically. The contract we're calling must be known at compile-time.

This constraint means any list of contracts a system can interact with, such as tokens supported by the Asset Manager, is fixed when the contract is created. In order to update the Asset Manager token list, both a new Asset Manager contract and a new xCall contract would need to be deployed. This is because the the Asset Manager needs to know about the new token and the xCall contract needs to know to call the new Asset Manager. It's like having to rebuild a whole bookshelf just to add one new book.

Upgradeability

This contract needs to be upgradeable, which is an ongoing barrier for Stacks adoption. However, it should be achievable and we can follow this implementation for reference on how to do it.

In short, there would be 3 contracts:

Proxy Contract: This would be the main entry point that doesn't change.
Implementation Contract: This contains the actual logic and token list, which can be updated.
Registry Contract: Keeps track of the current implementation contract.

;; Registry Contract
(define-data-var current-implementation principal 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.implementation-v1)

(define-public (update-implementation (new-implementation principal))
  (begin
    (asserts! (is-eq tx-sender contract-owner) (err u403))
    (var-set current-implementation new-implementation)
    (ok true)
  )
)

;; Proxy Contract
(define-public (handle-call-message (from (string-ascii 150)) (data (buff 1024)) (protocols (list 50 (string-ascii 150))))
  (contract-call? (unwrap! (contract-call? .registry get-current-implementation) (err u404))
                 handle-call-message from data protocols)
)

;; Implementation Contract V1
(define-public (handle-call-message (from (string-ascii 150)) (data (buff 1024)) (protocols (list 50 (string-ascii 150))))
  (let (
    (method-result (contract-call? .asset-manager-messages get-method data))
    (withdraw-to-name (contract-call? .asset-manager-messages get-withdraw-to-name))
  )
    (asserts! (is-ok method-result) ERR_INVALID_MESSAGE)
    (let ((method (unwrap-panic method-result)))
      (if (is-eq method withdraw-to-name)
        (let (
          (message-result (contract-call? .asset-manager-messages decode-withdraw-to data))
          (token-address-string (get token-address (unwrap-panic message-result)))
          (amount (get amount (unwrap-panic message-result)))
          (to-address-string (get to (unwrap-panic message-result)))
          (to-address-principal (contract-call? .util address-string-to-principal (unwrap-panic (as-max-len? to-address-string u128))))
        )
          (asserts! (is-ok message-result) ERR_INVALID_MESSAGE)
          (if (is-eq token-address-string "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc")
            (withdraw .sbtc amount (unwrap-panic to-address-principal))
            ERR_INVALID_TOKEN
          )
        )
        ERR_INVALID_MESSAGE
      )
    )
  )
)

;; Implementation Contract V2 (deployed later to add new tokens)
(define-public (handle-call-message (from (string-ascii 150)) (data (buff 1024)) (protocols (list 50 (string-ascii 150))))
  (let (
    (method-result (contract-call? .asset-manager-messages get-method data))
    (withdraw-to-name (contract-call? .asset-manager-messages get-withdraw-to-name))
  )
    (asserts! (is-ok method-result) ERR_INVALID_MESSAGE)
    (let ((method (unwrap-panic method-result)))
      (if (is-eq method withdraw-to-name)
        (let (
          (message-result (contract-call? .asset-manager-messages decode-withdraw-to data))
          (token-address-string (get token-address (unwrap-panic message-result)))
          (amount (get amount (unwrap-panic message-result)))
          (to-address-string (get to (unwrap-panic message-result)))
          (to-address-principal (contract-call? .util address-string-to-principal (unwrap-panic (as-max-len? to-address-string u128))))
        )
          (asserts! (is-ok message-result) ERR_INVALID_MESSAGE)
          (if (is-eq token-address-string "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sbtc")
            (withdraw .sbtc amount (unwrap-panic to-address-principal))
            (if (is-eq token-address-string "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.bnusd")
              (withdraw .bnusd amount (unwrap-panic to-address-principal))
              ERR_INVALID_TOKEN
            )
          )
        )
        ERR_INVALID_MESSAGE
      )
    )
  )
)

Asset Management

Implement a token registry system to manage supported tokens:

(define-map supported-tokens 
  principal 
  {
    period: uint,
    percentage: uint,
    last-update: uint,
    current-limit: uint
  }
)

Add and remove supported tokens (restricted to contract owner):

(define-constant CONTRACT_OWNER tx-sender)

(define-public (add-supported-token (token principal) (period uint) (percentage uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    ;; Add token to supported-tokens map and initialize rate limiting parameters
  )
)

(define-public (remove-supported-token (token principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    ;; Remove token from supported-tokens map
  )
)

Check balances for both STX and SIP-010 tokens:

(define-constant NATIVE_TOKEN 'STX)

(define-private (get-balance (token <ft-trait>))
  (if (is-eq (contract-of token) NATIVE_TOKEN)
      (ok (stx-get-balance (as-contract tx-sender)))
      (ok (unwrap! (contract-call? token get-balance (as-contract tx-sender)) ERR_INVALID_AMOUNT))
  )
)

Deposit

;; Function: deposit
;; Description: Handles deposits of tokens from other chains
;; Parameters:
;;   - from: (string-ascii 64) - The source address on the originating chain
;;   - token: principal - The principal of the token contract to deposit to
;;   - to: (optional principal) - The destination address on Stacks (if not provided, use a derived address from 'from')
;;   - amount: uint - The amount of tokens to deposit
;; Returns: (response bool uint) - OK with true if successful, ERR with error code if failed
(define-public (deposit (from (string-ascii 64)) (token principal) (to (optional principal)) (amount uint))
(begin
  ;; Implementation
  ;; 1. Verify the deposit message from the main Asset Manager
  ;; 2. Mint or unlock the tokens on the Stacks chain
  ;; 3. Transfer the tokens to the destination address
  (ok true)
)
)

;; Function: deposit-native
;; Description: Handles deposits of native STX from other chains
;; Parameters:
;;   - from: (string-ascii 64) - The source address on the originating chain
;;   - to: (optional principal) - The destination address on Stacks (if not provided, use a derived address from 'from')
;;   - amount: uint - The amount of STX to deposit
;; Returns: (response bool uint) - OK with true if successful, ERR with error code if failed
(define-public (deposit-native (from (string-ascii 64)) (to (optional principal)) (amount uint))
(begin
  ;; Implementation
  ;; 1. Verify the deposit message from the main Asset Manager
  ;; 2. Transfer the locked STX to the destination address
  (ok true)
)
)

Withdrawal

;; Function: withdraw-to
;; Description: Withdraws tokens from Stacks to be sent to another chain
;; Parameters:
;;   - token: principal - The principal of the token contract to withdraw
;;   - to: (string-ascii 64) - The destination address on the target chain
;;   - amount: uint - The amount of tokens to withdraw
;; Returns: (response bool uint) - OK with true if successful, ERR with error code if failed
(define-public (withdraw-to (token principal) (to (string-ascii 64)) (amount uint))
(begin
  ;; Implementation
  ;; 1. Burn or lock the tokens on the Stacks chain
  ;; 2. Prepare the withdrawal message for the main Asset Manager
  ;; 3. Call the cross-chain messaging service to send the withdrawal request
  (ok true)
)
)

;; Function: withdraw-native-to
;; Description: Withdraws native STX from Stacks to be sent to another chain
;; Parameters:
;;   - to: (string-ascii 64) - The destination address on the target chain
;;   - amount: uint - The amount of STX to withdraw
;; Returns: (response bool uint) - OK with true if successful, ERR with error code if failed
(define-public (withdraw-native-to (to (string-ascii 64)) (amount uint))
(begin
  ;; Implementation
  ;; 1. Lock the STX in the contract
  ;; 2. Prepare the withdrawal message for the main Asset Manager
  ;; 3. Call the cross-chain messaging service to send the withdrawal request
  (ok true)
)
)

Rate Limiting

  • Implement a configurable rate-limiting system for withdrawals:
    • Per-token configurable withdrawal limits
    • Time-based replenishment of withdrawal allowances
  • Functions to set and update rate limit parameters (owner-only)
  • Function to calculate current withdrawal limits
(define-map limit-map principal {
  period: uint,
  percentage: uint,
  last-update: uint,
  current-limit: uint
})

(define-public (configure-rate-limit (token <ft-trait>) (new-period uint) (new-percentage uint))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    (asserts! (<= new-percentage POINTS) ERR_INVALID_AMOUNT)
    (let ((balance (unwrap! (get-balance token) ERR_INVALID_AMOUNT)))
      (map-set limit-map (contract-of token) {
        period: new-period,
        percentage: new-percentage,
        last-update: block-height,
        current-limit: (/ (* balance new-percentage) POINTS)
      })
    )
    (ok true)
  )
)

(define-public (reset-limit (token <ft-trait>))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    (let ((balance (unwrap-panic (get-balance token))))
      (let ((period-tuple (unwrap-panic (map-get? limit-map (contract-of token)))))
        (map-set limit-map (contract-of token) (merge period-tuple {
          current-limit: (/ (* balance (get percentage period-tuple)) POINTS)
        }))
      )
    )
    (ok true)
  )
)
@CyrusVorwald CyrusVorwald added the enhancement New feature or request label Aug 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant