From 5a72b02f8eb7725232132b22f55377bcfcb3dafd Mon Sep 17 00:00:00 2001 From: caydyan Date: Tue, 23 Jun 2026 01:24:17 +0800 Subject: [PATCH] Lock standard stableswap minimum liquidity --- contracts/stableswap.clar | 17 +++++--- tests/stableswap_test.ts | 84 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/contracts/stableswap.clar b/contracts/stableswap.clar index 5f65c51..c190b6a 100644 --- a/contracts/stableswap.clar +++ b/contracts/stableswap.clar @@ -32,6 +32,9 @@ ;; Number of tokens per pair (define-constant number-of-tokens u2) +;; Permanently locked LP supply minted during pair creation +(define-constant minimum-liquidity u1000) + ;; Contract deployer (define-constant contract-deployer tx-sender) @@ -837,6 +840,7 @@ (scaled-up-balances (get-scaled-up-token-amounts initial-x-bal initial-y-bal x-decimals y-decimals)) (initial-x-bal-scaled (get scaled-x scaled-up-balances)) (initial-y-bal-scaled (get scaled-y scaled-up-balances)) + (initial-lp-supply (+ initial-x-bal-scaled initial-y-bal-scaled)) ) ;; Assert that tx-sender is an admin using is-some & index-of with the admins var @@ -854,8 +858,12 @@ ;; Assert that x & y tokens are the same (asserts! (is-eq initial-x-bal-scaled initial-y-bal-scaled) (err "err-initial-bal-odd")) - ;; Mint LP tokens to tx-sender - (unwrap! (as-contract (contract-call? lp-token mint lp-owner (+ initial-x-bal-scaled initial-y-bal-scaled))) (err "err-minting-lp-tokens")) + ;; Assert that the initial liquidity can cover the permanent lock + (asserts! (> initial-lp-supply minimum-liquidity) (err "err-initial-liquidity-too-low")) + + ;; Mint LP tokens to tx-sender and permanently lock minimum liquidity + (unwrap! (as-contract (contract-call? lp-token mint lp-owner (- initial-lp-supply minimum-liquidity))) (err "err-minting-lp-tokens")) + (unwrap! (as-contract (contract-call? lp-token mint this-contract minimum-liquidity)) (err "err-locking-minimum-liquidity")) ;; Transfer token x liquidity to this contract (unwrap! (contract-call? x-token transfer initial-x-bal tx-sender (as-contract tx-sender) none) (err "err-transferring-token-x")) @@ -866,12 +874,12 @@ ;; Update all appropriate maps (ok (map-set PairsDataMap {x-token: (contract-of x-token), y-token: (contract-of y-token), lp-token: (contract-of lp-token)} { approval: true, - total-shares: (+ initial-x-bal-scaled initial-y-bal-scaled), + total-shares: initial-lp-supply, x-decimals: x-decimals, y-decimals: y-decimals, balance-x: initial-x-bal, balance-y: initial-y-bal, - d: (+ initial-x-bal-scaled initial-y-bal-scaled), + d: initial-lp-supply, amplification-coefficient: amplification-coefficient, })) ) @@ -1032,4 +1040,3 @@ (ok staking-contract) ) ) - diff --git a/tests/stableswap_test.ts b/tests/stableswap_test.ts index 3a7b055..82afc2b 100644 --- a/tests/stableswap_test.ts +++ b/tests/stableswap_test.ts @@ -41,6 +41,88 @@ Clarinet.test({ }, }); +// Test pair creation permanently locks minimum liquidity +Clarinet.test({ + name: "Ensure creating a pair locks minimum liquidity", + async fn(chain: Chain, accounts: Map) { + const deployer = accounts.get("deployer")!; + + chain.mineBlock([ + Tx.contractCall("usda-token", "mint", [types.uint(100000000000000), types.principal(deployer.address)], deployer.address) + ]); + + chain.mineBlock([ + Tx.contractCall("susdt-token", "mint", [types.uint(10000000000000000), types.principal(deployer.address)], deployer.address) + ]); + + chain.mineBlock([ + Tx.contractCall("stableswap", "create-pair", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.susdt-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-susdt-lp-token"), types.uint(100), types.ascii("test"), types.uint(1000000000000000), types.uint(10000000000000)], deployer.address) + ]); + + const lockedLp = chain.callReadOnlyFn("usda-susdt-lp-token", "get-balance", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.stableswap")], deployer.address); + const ownerLp = chain.callReadOnlyFn("usda-susdt-lp-token", "get-balance", [types.principal(deployer.address)], deployer.address); + + lockedLp.result.expectOk().expectUint(1000); + ownerLp.result.expectOk().expectUint(1999999999999000); + }, +}); + +// Test pair creation rejects liquidity that cannot cover the permanent lock +Clarinet.test({ + name: "Ensure creating a pair rejects liquidity at or below the lock", + async fn(chain: Chain, accounts: Map) { + const deployer = accounts.get("deployer")!; + + chain.mineBlock([ + Tx.contractCall("usda-token", "mint", [types.uint(500), types.principal(deployer.address)], deployer.address) + ]); + + chain.mineBlock([ + Tx.contractCall("susdt-token", "mint", [types.uint(500), types.principal(deployer.address)], deployer.address) + ]); + + const block = chain.mineBlock([ + Tx.contractCall("stableswap", "create-pair", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.susdt-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-susdt-lp-token"), types.uint(100), types.ascii("test"), types.uint(500), types.uint(5)], deployer.address) + ]); + + block.receipts[0].result.expectErr().expectAscii("err-initial-liquidity-too-low"); + }, +}); + +// Test initial LP owner cannot fully empty the pool +Clarinet.test({ + name: "Ensure initial LP owner cannot withdraw locked minimum liquidity", + async fn(chain: Chain, accounts: Map) { + const deployer = accounts.get("deployer")!; + + chain.mineBlock([ + Tx.contractCall("usda-token", "mint", [types.uint(100000000000000), types.principal(deployer.address)], deployer.address) + ]); + + chain.mineBlock([ + Tx.contractCall("susdt-token", "mint", [types.uint(10000000000000000), types.principal(deployer.address)], deployer.address) + ]); + + chain.mineBlock([ + Tx.contractCall("stableswap", "create-pair", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.susdt-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-susdt-lp-token"), types.uint(100), types.ascii("test"), types.uint(1000000000000000), types.uint(10000000000000)], deployer.address) + ]); + + const block = chain.mineBlock([ + Tx.contractCall("stableswap", "withdraw-liquidity", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.susdt-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-susdt-lp-token"), types.uint(1999999999999000), types.uint(0), types.uint(0)], deployer.address) + ]); + + block.receipts[0].result.expectOk().expectTuple()["withdrawal-x-balance"].expectUint(999999999999500); + block.receipts[0].result.expectOk().expectTuple()["withdrawal-y-balance"].expectUint(9999999999995); + + const remainingPair = chain.callReadOnlyFn("stableswap", "get-pair-data", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.susdt-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.usda-susdt-lp-token")], deployer.address); + const pairTuple = remainingPair.result.expectSome().expectTuple(); + + pairTuple["balance-x"].expectUint(500); + pairTuple["balance-y"].expectUint(5); + pairTuple["total-shares"].expectUint(1000); + }, +}); + // Test get current cycle Clarinet.test({ name: "Ensure we can get the current cycle", @@ -1675,4 +1757,4 @@ Clarinet.test({ block.receipts[0].result.expectErr() console.log(JSON.stringify(block.receipts)); }, -}); \ No newline at end of file +});