Skip to content
Open
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
17 changes: 12 additions & 5 deletions contracts/stableswap.clar
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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"))
Expand All @@ -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,
}))
)
Expand Down Expand Up @@ -1032,4 +1040,3 @@
(ok staking-contract)
)
)

84 changes: 83 additions & 1 deletion tests/stableswap_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Account>) {
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<string, Account>) {
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<string, Account>) {
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",
Expand Down Expand Up @@ -1675,4 +1757,4 @@ Clarinet.test({
block.receipts[0].result.expectErr()
console.log(JSON.stringify(block.receipts));
},
});
});