Partial repayment & withdrawal [DEMO] - #161
Conversation
|
@Hrom131 could you move the Simplicity artifact generation into the Docker build? The flow in Would a builder stage in |
| const alreadyRepaidProtocolFee = getProtocolFee(alreadyRepaidFee) | ||
| const feeLeft = toUint64(totalFee - alreadyRepaidFee, 'feeLeft') | ||
| const feeRepaidNow = toUint64(minUint64(feeLeft, amountToRepay), 'feeRepaidNow') | ||
| const protocolFeeRepaidNow = getProtocolFee(feeRepaidNow) |
There was a problem hiding this comment.
This per-installment rounding disagrees with the covenant, which computes cumulative protocol fee after the repayment minus cumulative protocol fee before it. For example, fee portions of 15 then 5 should advance the protocol vault from 1 to 2, but this adds 0 on the second transaction and covenant finalization fails. Please calculate getProtocolFee(alreadyRepaidFee + feeRepaidNow) - alreadyRepaidProtocolFee.
| ) | ||
| // create-offer tx vout 2 = Borrower NFT (asset id needed for program reconstruction) — | ||
| // fetched once above via createOfferTx, reused here instead of a second round trip. | ||
| const borrowerNftTxOut = requireTxOut(createOfferTx, 2, 'Borrower NFT reference') |
There was a problem hiding this comment.
Reusing the creation transaction fixes the borrower NFT lookup, but the reconstruction below still uses the active output's reduced collateral as compile-time collateralAmount and defaults state to the original total debt. Therefore an expired offer cannot be liquidated after a partial repayment. Please recover original collateral from the creation output and pass the indexer's current debt to both the spend info and liquidation witness.
Review fix verificationcreate-offer txid Cumulative protocol fee rounding (
|
| Case | Phase | amountToRepay | Result | Tx | Status |
|---|---|---|---|---|---|
| 1 | NoRepayments → RepayingOfferFee | 15 | Protocol-fee vault created active (alreadySupplied=1), lender vault active (alreadySupplied=14) |
0a8f3d4556... |
✅ |
| 2 | RepayingOfferFee | 5 | Protocol-fee vault 1 → 2 (floor(20*0.1) - floor(15*0.1) = 1, cumulative), lender vault 14 → 18 |
ab156dcd01... |
✅ |
Before the fix, case 2 computed protocolFeeRepaidNow = getProtocolFee(5) = 0 (old per-installment formula), producing a vault output the covenant rejects.
Second consecutive WithdrawPart, no repay/supply in between (useLenderVaultWithdrawPart.ts)
| Case | Vault before | Vault producer | amountToWithdraw | Result | Tx | Status |
|---|---|---|---|---|---|---|
| 1 | Active, balance 18, alreadySupplied=18 |
repay tx (ab156dcd...) |
8 | Balance 18 → 10, alreadySupplied unchanged (18) |
4c438569e9... |
✅ |
| 2 | Active, balance 10, alreadySupplied=18 |
another WithdrawPart (4c438569...) |
3 | Balance 10 → 7, alreadySupplied unchanged (18) |
1ea8f33636... |
✅ |
Case 2 is the regression check: the vault being spent was produced by a prior WithdrawPart, not a repay/supply tx. Before the fix, the borrower-NFT asset id was read from inputs[0] of the vault's producing tx — correct only when that producer is a repay/supply tx (borrower NFT always at input 0 there). For a producer that's itself a WithdrawPart, inputs[0] is the previous vault, not the borrower NFT, so the reconstructed program — and the on-chain script hash check — would have failed. Fixed by reading the borrower NFT asset from createOfferTx vout 2 instead, which is producer-independent.
Blocking1. Liquidation after partial repayment can permanently lock the lender vault balance
After a partial repayment, this vault contains repaid principal. Please prevent liquidation while an active lender vault exists, or update the protocol so liquidation atomically finalizes or claims that vault. Important2. Full repayment after partial repayment underestimates the required fee
As a result, fee selection at Please add the 3. Repayment confirmation displays the original amounts
The confirmation should reflect the amounts the transaction will move. 4. Liquidation confirmation displays the original collateral
|
1 — confirmed, real fund-loss gap. Root cause is the covenant: WithdrawPart can never empty the vault (amount_to_withdraw < vault_amount), WithdrawAll needs finalized (only via full repay) — mutually exclusive with liquidation. No app-level fix. Discussed with @Hrom131 , proper fix lands in V2 (atomic claim on liquidation). Short-term: warn lender to claim vault first, no hard block. 2 — confirmed bug, separate task (dynamic fee weight instead of static constant). 3/4 — won't-fix, these modals are getting replaced next design pass. |
Partial repayment test matrix
principalAmount = 10000, principalInterestRate = 1000 (10%) → totalFee = 1000, totalProtocolFee = 100, totalAmountToRepay = 11000. collateralAmount = 5000.
Offers
Vault withdraw test matrix
Offer 6: principalAmount = 10000, principalInterestRate = 1000 (10%) → totalFee = 1000, totalProtocolFee = 100, totalAmountToRepay = 11000. collateralAmount = 5000.