Make a Sui temporary withdrawal leave a mandatory receipt

A module can return a receipt with no abilities alongside a temporary withdrawal. The caller has to consume that value through an allowed path before the transaction can finish.

Bind the receipt to the original vault and obligation

This excerpt illustrates the identity and amount checks only. In a real repayment function, derive paid from the actual asset deposited in the vault. Allowing the caller to supply a number without making the deposit would defeat repayment even if the receipt were linear.

public struct BorrowReceipt { vault: ID, due: u64 }
public fun finish(receipt: BorrowReceipt, vault: &Vault, paid: u64) {
    let BorrowReceipt { vault: expected, due } = receipt;
    assert!(expected == object::id(vault) && paid == due, 0);
}

Exercise every path that consumes the receipt

A receipt from vault A for 50 units must fail against vault B or a deposit of 49. Omitting the finish step should leave an unused non-droppable value and fail the transaction. Audit every function in the defining module that destructures BorrowReceipt, since each such path can discharge the obligation.

Sources