Skip to content
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
5 changes: 5 additions & 0 deletions contracts/Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ path = "contracts/traits/stackingdao-ststx-trait.clar"
clarity_version = 2
epoch = 2.4

[contracts.stackingdao-direct-helpers-trait]
path = "contracts/traits/stackingdao-direct-helpers-trait.clar"
clarity_version = 2
epoch = 2.4

[contracts.hermetica-usdh-trait]
path = "contracts/traits/hermetica-usdh-trait.clar"
clarity_version = 2
Expand Down
53 changes: 39 additions & 14 deletions contracts/contracts/adapters/stackingdao-adapter.clar
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
;; @notice Routes V-Mind vault interactions to StackingDAO stSTX contracts.

(impl-trait .protocol-adapter-trait.protocol-adapter-trait)
(use-trait stackingdao-direct-helpers-trait .stackingdao-direct-helpers-trait.stackingdao-direct-helpers-trait)

(define-constant one-8 u100000000)

Expand Down Expand Up @@ -31,6 +32,7 @@

(define-data-var total-ststx-shares uint u0)
(define-data-var total-principal-tracked uint u0)
(define-data-var cached-live-total-underlying (optional uint) none)

(define-map vault-positions
{ vault-id: uint }
Expand Down Expand Up @@ -81,13 +83,28 @@
(map-set vault-positions { vault-id: vault-id } { ststx-shares: shares, stx-principal-deployed: principal-deployed })
)

(define-private (refresh-live-total-underlying (helpers <stackingdao-direct-helpers-trait>))
(match (contract-call? helpers get-user-balance-in-protocol (adapter-principal) (var-get staking-contract) u0)
amount
(begin
(var-set cached-live-total-underlying (some amount))
(ok amount)
)
external-err err-external-call-failed
)
)

(define-public (sync-live-total-underlying (helpers <stackingdao-direct-helpers-trait>))
(begin
(try! (assert-configured))
(refresh-live-total-underlying helpers)
)
)

(define-private (get-total-underlying)
(if (var-get use-mock)
(match (contract-call? .mock-stackingdao-core get-user-balance-in-protocol (adapter-principal) (var-get staking-contract) u0)
amount amount
helper-err (var-get total-principal-tracked)
)
(var-get total-principal-tracked)
(match (var-get cached-live-total-underlying)
amount (ok amount)
err-external-call-failed
)
)

Expand Down Expand Up @@ -283,19 +300,23 @@
(ok (get ststx-shares (get-position vault-id)))
)

(define-read-only (get-live-total-underlying)
(get-total-underlying)
)

(define-read-only (get-ststx-exchange-rate)
(begin
(try! (assert-configured))
(let
(
(total-shares (var-get total-ststx-shares))
(total-underlying (get-total-underlying))
)
(ok
(if (is-eq total-shares u0)
one-8
(/ (* total-underlying one-8) total-shares)
(
(total-underlying (try! (get-total-underlying)))
(total-shares (var-get total-ststx-shares))
)
(ok
(if (is-eq total-shares u0)
one-8
(/ (* total-underlying one-8) total-shares)
)
)
)
)
Expand All @@ -315,6 +336,10 @@
(ok (var-get use-mock))
)

(define-read-only (get-configured-stackingdao-helper-contract)
(ok (var-get helpers-contract))
)

(define-read-only (is-configured)
(configuration-ready)
)
Expand Down
2 changes: 2 additions & 0 deletions contracts/contracts/mocks/mock-stackingdao-core.clar
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
;; @version 2026-04-10 added deterministic failure toggles and reconciliation safety banner
;; @notice Local test double for StackingDAO core/reserve/helper interfaces.

(impl-trait .stackingdao-direct-helpers-trait.stackingdao-direct-helpers-trait)

(define-constant one-8 u100000000)

(define-constant err-forced-failure (err u8201))
Expand Down
10 changes: 10 additions & 0 deletions contracts/contracts/traits/stackingdao-direct-helpers-trait.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
;; @title V-Mind StackingDAO Direct Helpers Trait
;; @version 0.1.0
;; @author V-Mind Core Team
;; @notice Trait for authoritative StackingDAO balance reads through direct helpers.

(define-trait stackingdao-direct-helpers-trait
(
(get-user-balance-in-protocol (principal principal uint) (response uint uint))
)
)
49 changes: 39 additions & 10 deletions contracts/tests/stackingdao-adapter_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,6 @@ Clarinet.test({
},
});

Clarinet.test({
name: 'stackingdao-adapter: returns recoverable errors for balance reads before configuration',
async fn(chain: Chain, accounts: Map<string, Account>) {
const deployer = accounts.get('deployer')!;

const balance = chain.callReadOnlyFn('stackingdao-adapter', 'get-vault-stx-balance', [types.uint(3)], deployer.address);
balance.result.expectErr().expectUint(3608);
},
});

Clarinet.test({
name: 'stackingdao-adapter: reports STX balances using exchange-rate aware accounting',
async fn(chain: Chain, accounts: Map<string, Account>) {
Expand All @@ -78,12 +68,51 @@ Clarinet.test({
Tx.contractCall('stackingdao-adapter', 'mint-ststx', [types.uint(9), types.uint(1_000_000)], deployer.address),
Tx.contractCall('stackingdao-adapter', 'redeem-ststx', [types.uint(9), types.uint(400_000)], deployer.address),
Tx.contractCall('mock-stackingdao-core', 'set-exchange-rate', [types.uint(120_000_000)], deployer.address),
Tx.contractCall('stackingdao-adapter', 'sync-live-total-underlying', [mock(deployer)], deployer.address),
]);

block.receipts[1].result.expectOk().expectUint(1_000_000);
block.receipts[2].result.expectOk().expectUint(400_000);
block.receipts[5].result.expectOk().expectUint(720_000);

const tracked = chain.callReadOnlyFn('stackingdao-adapter', 'get-total-principal-tracked', [], deployer.address);
tracked.result.expectOk().expectUint(600_000);

const liveTotal = chain.callReadOnlyFn('stackingdao-adapter', 'get-live-total-underlying', [], deployer.address);
liveTotal.result.expectOk().expectUint(720_000);

const balance = chain.callReadOnlyFn('stackingdao-adapter', 'get-vault-stx-balance', [types.uint(9)], deployer.address);
balance.result.expectOk().expectUint(720_000);
},
});

Clarinet.test({
name: 'stackingdao-adapter: surfaces live read failures in non-mock mode',
async fn(chain: Chain, accounts: Map<string, Account>) {
const deployer = accounts.get('deployer')!;

const block = chain.mineBlock([
Tx.contractCall('stackingdao-adapter', 'set-mock-mode', [types.bool(false)], deployer.address),
Tx.contractCall(
'stackingdao-adapter',
'set-stackingdao-config',
[mock(deployer), mock(deployer), mock(deployer), mock(deployer), mock(deployer)],
deployer.address,
),
Tx.contractCall(
'stackingdao-adapter',
'sync-live-total-underlying',
[types.principal(`${deployer.address}.missing-stackingdao-helper`)],
deployer.address,
),
]);

block.receipts[2].result.expectErr().expectUint(3603);

const balance = chain.callReadOnlyFn('stackingdao-adapter', 'get-vault-stx-balance', [types.uint(7)], deployer.address);
balance.result.expectErr().expectUint(3603);

const liveTotal = chain.callReadOnlyFn('stackingdao-adapter', 'get-live-total-underlying', [], deployer.address);
liveTotal.result.expectErr().expectUint(3603);
},
});
Loading