-
Notifications
You must be signed in to change notification settings - Fork 2
[ALPHANET] Coupon Payments #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| #pragma once | ||
|
|
||
| #include <xrpl/beast/utility/Journal.h> | ||
| #include <xrpl/ledger/ApplyView.h> | ||
| #include <xrpl/ledger/ReadView.h> | ||
| #include <xrpl/protocol/AccountID.h> | ||
| #include <xrpl/protocol/STAmount.h> | ||
| #include <xrpl/protocol/STLedgerEntry.h> | ||
| #include <xrpl/protocol/TER.h> | ||
| #include <xrpl/protocol/XRPAmount.h> | ||
|
|
||
| #include <cstdint> | ||
| #include <optional> | ||
|
|
||
| namespace xrpl { | ||
|
|
||
| /** | ||
| * Settle coupon accrual on a registration. | ||
| * | ||
| * Computes the coupon dates elapsed since the registration's | ||
| * LastSettledTime (strictly before the schedule's Expiration, if any), | ||
| * adds count * RegisteredUnits * CouponAmount to AccruedAmount — | ||
| * rounded once, downward, to the coupon asset's precision — and | ||
| * advances LastSettledTime to the latest elapsed date. O(1) in the | ||
| * number of elapsed dates. The caller must view.update() the | ||
| * registration on tesSUCCESS. | ||
| */ | ||
| [[nodiscard]] TER | ||
| couponSettle( | ||
| SLE::const_ref schedule, | ||
| SLE::ref registration, | ||
| std::uint32_t parentCloseTime, | ||
| beast::Journal j); | ||
|
|
||
| /** | ||
| * Eligibility for locking bond units into a registration. | ||
| * | ||
| * Mirrors the token Escrow lock gates: issuer opt-in | ||
| * (lsfAllowTrustLineLocking / lsfMPTCanEscrow), authorization, freeze | ||
| * and lock state, and the holder's spendable balance. | ||
| */ | ||
| [[nodiscard]] TER | ||
| couponLockPreclaim( | ||
| ReadView const& view, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The IOU specialization of the lock preclaim uses |
||
| AccountID const& holder, | ||
| STAmount const& units, | ||
| beast::Journal j); | ||
|
|
||
| /** | ||
| * Lock bond units: IOU balances park on the bond issuer's trust line, | ||
| * MPT amounts move into sfLockedAmount — the token Escrow mechanics. | ||
| */ | ||
| [[nodiscard]] TER | ||
| couponLockUnits(ApplyView& view, AccountID const& holder, STAmount const& units, beast::Journal j); | ||
|
|
||
| /** | ||
| * Release locked bond units back to the holder's spendable balance. | ||
| * No transfer fee applies (round trip to self). | ||
| */ | ||
| [[nodiscard]] TER | ||
| couponUnlockUnits( | ||
| ApplyViewContext ctx, | ||
| SLE::ref sleHolder, | ||
| XRPAmount preFeeBalance, | ||
| STAmount const& units, | ||
| AccountID const& holder, | ||
| beast::Journal j); | ||
|
|
||
| /** | ||
| * Delete a registration if its RegisteredUnits and AccruedAmount are | ||
| * both zero: remove it from the holder's directory, refund the owner | ||
| * reserve, and decrement the schedule's RegistrationCount. The caller | ||
| * must view.update() the schedule when a deletion is reported. | ||
| * | ||
| * @return std::nullopt if the registration is not empty (not deleted); | ||
| * tesSUCCESS if it was deleted; an error TER on failure. | ||
| */ | ||
| [[nodiscard]] std::optional<TER> | ||
| couponDeleteRegistrationIfEmpty( | ||
| ApplyView& view, | ||
| SLE::ref schedule, | ||
| SLE::ref registration, | ||
| AccountID const& holder, | ||
| beast::Journal j); | ||
|
|
||
| } // namespace xrpl | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,6 +119,12 @@ TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) | |
| TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) | ||
| TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73) | ||
| TYPED_SFIELD(sfSponsorFlags, UINT32, 74) | ||
| TYPED_SFIELD(sfCouponInterval, UINT32, 75) | ||
| TYPED_SFIELD(sfFirstCouponTime, UINT32, 76) | ||
| TYPED_SFIELD(sfCallNoticePeriod, UINT32, 77) | ||
| TYPED_SFIELD(sfEarliestCallTime, UINT32, 78) | ||
| TYPED_SFIELD(sfLastSettledTime, UINT32, 79) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sfEarliestCallTime accepts 0 and any past timestamp — there is no check that it is non-zero, greater than the current ledger close time, or at or after sfFirstCouponTime. The call-protection enforcement in CouponScheduleSet compares newExpiration < sfEarliestCallTime, so when sfEarliestCallTime is 0 that comparison is trivially false against any real timestamp, rendering the call-protection window completely non-functional. An issuer can create a callable bond with sfFirstCouponTime one year out and sfEarliestCallTime = 0, attract holders who lock their units via CouponRegister, then immediately submit CouponScheduleSet to expire the bond one notice-period later — before a single coupon is ever paid — while investors cannot recover their principal until they submit CouponUnregister themselves. Add a preflight rejection for sfEarliestCallTime == 0 and sfEarliestCallTime < sfFirstCouponTime, and a preclaim rejection for sfEarliestCallTime <= parentCloseTime, consistent with how sfFirstCouponTime is already validated against the live ledger time. Suggested fixIn CouponScheduleCreate::preflight: (1) reject sfEarliestCallTime == 0 with temMALFORMED; (2) reject sfEarliestCallTime < sfFirstCouponTime with temBAD_EXPIRATION. In preclaim: (3) reject sfEarliestCallTime <= parentCloseTime.time_since_epoch().count() with tecEXPIRED, mirroring the existing sfFirstCouponTime check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sfEarliestCallTime accepts 0 and any past timestamp — there is no check that it is non-zero, greater than the current ledger close time, or at or after sfFirstCouponTime. The call-protection enforcement in CouponScheduleSet compares newExpiration < sfEarliestCallTime, so when sfEarliestCallTime is 0 that comparison is trivially false against any real timestamp, rendering the call-protection window completely non-functional. An issuer can create a callable bond with sfFirstCouponTime one year out and sfEarliestCallTime = 0, attract holders who lock their units via CouponRegister, then immediately submit CouponScheduleSet to expire the bond one notice-period later — before a single coupon is ever paid — while investors cannot recover their principal until they submit CouponUnregister themselves. Add a preflight rejection for sfEarliestCallTime == 0 and sfEarliestCallTime < sfFirstCouponTime, and a preclaim rejection for sfEarliestCallTime <= parentCloseTime, consistent with how sfFirstCouponTime is already validated against the live ledger time. Suggested fixIn CouponScheduleCreate::preflight: (1) reject sfEarliestCallTime == 0 with temMALFORMED; (2) reject sfEarliestCallTime < sfFirstCouponTime with temBAD_EXPIRATION. In preclaim: (3) reject sfEarliestCallTime <= parentCloseTime.time_since_epoch().count() with tecEXPIRED, mirroring the existing sfFirstCouponTime check. |
||
| TYPED_SFIELD(sfRegistrationCount, UINT32, 80) | ||
|
|
||
| // 64-bit integers (common) | ||
| TYPED_SFIELD(sfIndexNext, UINT64, 1) | ||
|
|
@@ -216,6 +222,7 @@ TYPED_SFIELD(sfLoanID, UINT256, 38) | |
| TYPED_SFIELD(sfReferenceHolding, UINT256, 39) | ||
| TYPED_SFIELD(sfBlindingFactor, UINT256, 40) | ||
| TYPED_SFIELD(sfObjectID, UINT256, 41) | ||
| TYPED_SFIELD(sfCouponScheduleID, UINT256, 42) | ||
|
|
||
| // number (common) | ||
| TYPED_SFIELD(sfNumber, NUMBER, 1) | ||
|
|
@@ -277,6 +284,9 @@ TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) | |
| TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) | ||
| TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) | ||
| TYPED_SFIELD(sfMaxFee, AMOUNT, 33) | ||
| TYPED_SFIELD(sfCouponAmount, AMOUNT, 34) | ||
| TYPED_SFIELD(sfRegisteredUnits, AMOUNT, 35) | ||
| TYPED_SFIELD(sfAccruedAmount, AMOUNT, 36) | ||
|
|
||
| // variable length (common) | ||
| TYPED_SFIELD(sfPublicKey, VL, 1) | ||
|
|
@@ -377,6 +387,7 @@ TYPED_SFIELD(sfLockingChainIssue, ISSUE, 1) | |
| TYPED_SFIELD(sfIssuingChainIssue, ISSUE, 2) | ||
| TYPED_SFIELD(sfAsset, ISSUE, 3) | ||
| TYPED_SFIELD(sfAsset2, ISSUE, 4) | ||
| TYPED_SFIELD(sfBondAsset, ISSUE, 5) | ||
|
|
||
| // bridge | ||
| TYPED_SFIELD(sfXChainBridge, XCHAIN_BRIDGE, 1) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| #pragma once | ||
|
|
||
| #include <xrpl/beast/utility/Journal.h> | ||
| #include <xrpl/ledger/ReadView.h> | ||
| #include <xrpl/protocol/STTx.h> | ||
| #include <xrpl/protocol/TER.h> | ||
| #include <xrpl/protocol/XRPAmount.h> | ||
| #include <xrpl/tx/ApplyContext.h> | ||
| #include <xrpl/tx/Transactor.h> | ||
|
|
||
| namespace xrpl { | ||
|
|
||
| class CouponClaim : public Transactor | ||
| { | ||
| public: | ||
| static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; | ||
|
|
||
| explicit CouponClaim(ApplyContext& ctx) : Transactor(ctx) | ||
| { | ||
| } | ||
|
|
||
| static NotTEC | ||
| preflight(PreflightContext const& ctx); | ||
|
|
||
| static TER | ||
| preclaim(PreclaimContext const& ctx); | ||
|
|
||
| TER | ||
| doApply() override; | ||
|
|
||
| void | ||
| visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; | ||
|
|
||
| [[nodiscard]] bool | ||
| finalizeInvariants( | ||
| STTx const& tx, | ||
| TER result, | ||
| XRPAmount fee, | ||
| ReadView const& view, | ||
| beast::Journal const& j) override; | ||
| }; | ||
|
|
||
| } // namespace xrpl |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| #pragma once | ||
|
|
||
| #include <xrpl/beast/utility/Journal.h> | ||
| #include <xrpl/ledger/ReadView.h> | ||
| #include <xrpl/protocol/STTx.h> | ||
| #include <xrpl/protocol/TER.h> | ||
| #include <xrpl/protocol/XRPAmount.h> | ||
| #include <xrpl/tx/ApplyContext.h> | ||
| #include <xrpl/tx/Transactor.h> | ||
|
|
||
| namespace xrpl { | ||
|
|
||
| class CouponRegister : public Transactor | ||
| { | ||
| public: | ||
| static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; | ||
|
|
||
| explicit CouponRegister(ApplyContext& ctx) : Transactor(ctx) | ||
| { | ||
| } | ||
|
|
||
| static NotTEC | ||
| preflight(PreflightContext const& ctx); | ||
|
|
||
| static TER | ||
| preclaim(PreclaimContext const& ctx); | ||
|
|
||
| TER | ||
| doApply() override; | ||
|
|
||
| void | ||
| visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; | ||
|
|
||
| [[nodiscard]] bool | ||
| finalizeInvariants( | ||
| STTx const& tx, | ||
| TER result, | ||
| XRPAmount fee, | ||
| ReadView const& view, | ||
| beast::Journal const& j) override; | ||
| }; | ||
|
|
||
| } // namespace xrpl |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The accrual computation constructs
additionAmtviaSTAmount{couponAmount.asset(), addition}(CouponHelpers.cpp:68) before thecanAddoverflow guard on the next line has any chance to run. Whencount * RegisteredUnits * CouponAmountexceeds the asset's representable range — which is fully attacker-controlled via registration units and elapsed intervals — theSTAmountconstructor throwsstd::overflow_errororstd::runtime_error. None of the three transactors that callcouponSettle(CouponClaim, CouponRegister, CouponUnregister) catch this exception, so it bubbles to the outerapplyStepshandler and becomestefEXCEPTION. BecausesfLastSettledTimeis written on lines 77-78, after the throw, it never advances — and sinceCouponUnregister::doApplycallscouponSettlebefore releasing locked units, the holder can neither claim nor exit, permanently locking their registered bond units. Fix: bound-check theNumbermultiplication before constructing the STAmount, or wrap the construction in a try/catch that translates totecPRECISION_LOSS.