diff --git a/contracts/reservoir.clar b/contracts/reservoir.clar index 0e585c7..7ab45b8 100644 --- a/contracts/reservoir.clar +++ b/contracts/reservoir.clar @@ -439,6 +439,55 @@ ) ) +;;; Create a new tap and immediately borrow liquidity from the reservoir in a +;;; single transaction. This is a convenience function combining `create-tap` +;;; and `borrow-liquidity` for the common setup case where a new participant +;;; wants both outgoing capacity (their own deposit) and incoming capacity +;;; (borrowed liquidity) at once. +;;; +;;; The caller must first obtain `reservoir-signature` from the reservoir +;;; operator off-chain, confirming the post-borrow balances. +;;; +;;; Parameters: +;;; - `stackflow`: the StackFlow token contract +;;; - `token`: optional SIP-010 token (none for STX) +;;; - `tap-amount`: amount the caller deposits to fund their sending side +;;; - `tap-nonce`: nonce for the initial tap creation (typically u0) +;;; - `borrow-amount`: amount of liquidity to borrow from the reservoir +;;; - `borrow-fee`: fee paid to the reservoir for the borrow +;;; (must be >= get-borrow-fee(borrow-amount)) +;;; - `my-balance`: caller's balance after the borrow deposit +;;; (should equal tap-amount since no transfers have occurred yet) +;;; - `reservoir-balance`: reservoir's balance after the borrow deposit +;;; (should equal borrow-amount) +;;; - `my-signature`: caller's SIP-018 signature over the post-borrow state +;;; - `reservoir-signature`: reservoir's SIP-018 signature over the post-borrow state +;;; - `borrow-nonce`: nonce for the borrow deposit (must be > tap-nonce) +;;; +;;; Returns: +;;; - `(ok expire-block)` on success, where expire-block is the burn block +;;; height at which the borrowed liquidity expires +;;; - Any error from `create-tap` or `borrow-liquidity` +(define-public (create-tap-with-borrowed-liquidity + (stackflow ) + (token (optional )) + (tap-amount uint) + (tap-nonce uint) + (borrow-amount uint) + (borrow-fee uint) + (my-balance uint) + (reservoir-balance uint) + (my-signature (buff 65)) + (reservoir-signature (buff 65)) + (borrow-nonce uint) + ) + (begin + (try! (create-tap stackflow token tap-amount tap-nonce)) + (borrow-liquidity stackflow borrow-amount borrow-fee token my-balance + reservoir-balance my-signature reservoir-signature borrow-nonce) + ) +) + ;; ----- Read-only functions ----- ;;; Calculate the fee for borrowing a given amount. diff --git a/tests/reservoir.test.ts b/tests/reservoir.test.ts index 2b497f9..06f030a 100644 --- a/tests/reservoir.test.ts +++ b/tests/reservoir.test.ts @@ -1217,6 +1217,133 @@ describe("reservoir", () => { }); }); + describe("create-tap-with-borrowed-liquidity", () => { + beforeEach(() => { + // Set rate to 10% and add liquidity + simnet.callPublicFn( + "reservoir", + "set-borrow-rate", + [Cl.uint(1000)], + deployer + ); + simnet.callPublicFn( + "reservoir", + "add-liquidity", + [Cl.none(), Cl.uint(5000000000)], + deployer + ); + }); + + it("creates a tap and borrows liquidity in one call", () => { + const tapAmount = 1000000; + const tapNonce = 0; + const borrowAmount = 50000; + const borrowFee = 5000; // 10% + const borrowNonce = 1; + + const mySignature = generateDepositSignature( + address1PK, + null, + address1, + reservoirContract, + tapAmount, + borrowAmount, + borrowNonce, + reservoirContract + ); + + const reservoirSignature = generateDepositSignature( + deployerPK, + null, + reservoirContract, + address1, + borrowAmount, + tapAmount, + borrowNonce, + reservoirContract + ); + + const { result } = simnet.callPublicFn( + "reservoir", + "create-tap-with-borrowed-liquidity", + [ + Cl.principal(stackflowContract), + Cl.none(), + Cl.uint(tapAmount), + Cl.uint(tapNonce), + Cl.uint(borrowAmount), + Cl.uint(borrowFee), + Cl.uint(tapAmount), + Cl.uint(borrowAmount), + Cl.buffer(mySignature), + Cl.buffer(reservoirSignature), + Cl.uint(borrowNonce), + ], + address1 + ); + expect(result).toBeOk( + Cl.uint(simnet.burnBlockHeight + BORROW_TERM_BLOCKS) + ); + + // Verify balances: reservoir funded tap + borrow, minus borrow amount + fee + const stxBalances = simnet.getAssetsMap().get("STX")!; + // reservoir: 5000000000 - borrowAmount + borrowFee + expect(stxBalances.get(reservoirContract)).toBe(4999955000n); + // stackflow: tapAmount + borrowAmount + expect(stxBalances.get(stackflowContract)).toBe(1050000n); + }); + + it("fails if borrow signature is wrong", () => { + const tapAmount = 1000000; + const borrowAmount = 50000; + const borrowFee = 5000; + const borrowNonce = 1; + + // Use a wrong signature (signed with address2's key instead of address1's) + const wrongMySignature = generateDepositSignature( + address2PK, + null, + address1, + reservoirContract, + tapAmount, + borrowAmount, + borrowNonce, + reservoirContract + ); + + const reservoirSignature = generateDepositSignature( + deployerPK, + null, + reservoirContract, + address1, + borrowAmount, + tapAmount, + borrowNonce, + reservoirContract + ); + + const { result } = simnet.callPublicFn( + "reservoir", + "create-tap-with-borrowed-liquidity", + [ + Cl.principal(stackflowContract), + Cl.none(), + Cl.uint(tapAmount), + Cl.uint(0), + Cl.uint(borrowAmount), + Cl.uint(borrowFee), + Cl.uint(tapAmount), + Cl.uint(borrowAmount), + Cl.buffer(wrongMySignature), + Cl.buffer(reservoirSignature), + Cl.uint(borrowNonce), + ], + address1 + ); + expect(result).toBeErr(Cl.uint(StackflowError.InvalidSignature)); + }); + }); + describe("force-closures", () => { beforeEach(() => { // Add liquidity to reservoir