diff --git a/contracts/stableswap-stackingDAO.clar b/contracts/stableswap-stackingDAO.clar index 2897f6b..1fb9dbf 100644 --- a/contracts/stableswap-stackingDAO.clar +++ b/contracts/stableswap-stackingDAO.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) @@ -872,6 +875,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 @@ -886,8 +890,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! (stx-transfer? initial-x-bal tx-sender (as-contract tx-sender)) (err "err-transferring-token-x")) @@ -898,7 +906,7 @@ ;; Update all appropriate maps (ok (map-set PairsDataMap {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, diff --git a/tests/stableswap-stackingDAO_test.ts b/tests/stableswap-stackingDAO_test.ts index 4e75bb3..02deaf7 100644 --- a/tests/stableswap-stackingDAO_test.ts +++ b/tests/stableswap-stackingDAO_test.ts @@ -42,6 +42,76 @@ 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("ststx-token", "mint", [types.uint(100000000000000), types.principal(deployer.address)], deployer.address) + ]); + + chain.mineBlock([ + Tx.contractCall("stableswap-stackingDAO", "create-pair", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ststx-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.stx-ststx-lp-token"), types.uint(100), types.ascii("test"), types.uint(10000000000000), types.uint(10000000000000)], deployer.address) + ]); + + const lockedLp = chain.callReadOnlyFn("stx-ststx-lp-token", "get-balance", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.stableswap-stackingDAO")], deployer.address); + const ownerLp = chain.callReadOnlyFn("stx-ststx-lp-token", "get-balance", [types.principal(deployer.address)], deployer.address); + + lockedLp.result.expectOk().expectUint(1000); + ownerLp.result.expectOk().expectUint(19999999999000); + }, +}); + +// 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("ststx-token", "mint", [types.uint(500), types.principal(deployer.address)], deployer.address) + ]); + + const block = chain.mineBlock([ + Tx.contractCall("stableswap-stackingDAO", "create-pair", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ststx-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.stx-ststx-lp-token"), types.uint(100), types.ascii("test"), types.uint(500), types.uint(500)], 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("ststx-token", "mint", [types.uint(100000000000000), types.principal(deployer.address)], deployer.address) + ]); + + chain.mineBlock([ + Tx.contractCall("stableswap-stackingDAO", "create-pair", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ststx-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.stx-ststx-lp-token"), types.uint(100), types.ascii("test"), types.uint(10000000000000), types.uint(10000000000000)], deployer.address) + ]); + + const block = chain.mineBlock([ + Tx.contractCall("stableswap-stackingDAO", "withdraw-liquidity", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ststx-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.stx-ststx-lp-token"), types.uint(19999999999000), types.uint(0), types.uint(0)], deployer.address) + ]); + + block.receipts[0].result.expectOk().expectTuple()["withdrawal-x-balance"].expectUint(9999999999500); + block.receipts[0].result.expectOk().expectTuple()["withdrawal-y-balance"].expectUint(9999999999500); + + const remainingPair = chain.callReadOnlyFn("stableswap-stackingDAO", "get-pair-data", [types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.ststx-token"), types.principal("ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.stx-ststx-lp-token")], deployer.address); + const pairTuple = remainingPair.result.expectSome().expectTuple(); + + pairTuple["balance-x"].expectUint(500); + pairTuple["balance-y"].expectUint(500); + pairTuple["total-shares"].expectUint(1000); + }, +}); + // // Test get current cycle // Clarinet.test({ // name: "Ensure we can get the current cycle", @@ -1606,4 +1676,4 @@ Clarinet.test({ block.receipts[0].result.expectErr() console.log(JSON.stringify(block.receipts)); }, -}); \ No newline at end of file +});