Repository navigation
fix(remittance_nft): emit old/new value events from the two parameter setters (#1146) - #1950
Merged
K1NGD4VID merged 6 commits intoOct 6, 2026
Merged
Conversation
… setters (LabsCrypt#1146) set_default_burn_threshold and set_min_repayment_amount mutate protocol-critical instance config but published nothing, so off-chain consumers (indexer, webhooks, audit trails) could not observe changes to either risk parameter. Every other admin action in this contract (authorize_minter, revoke_minter, pause, unpause, admin transfer, set_loan_manager) already emits an event. Both setters now capture the outgoing value before the overwrite and publish the full old -> new transition after the state write: set_default_burn_threshold -> topics (DefaultBurnThresholdUpdated, admin) data (old_threshold, new_threshold) set_min_repayment_amount -> topics (MinRepaymentUpdated, admin) data (old_amount, new_amount) The topics/payload shape follows the admin config-update convention already documented in contracts/loan_manager/src/events.rs and already decoded by backend/src/services/eventIndexer.ts for MinRepaymentUpdated, so no indexer change is required. Auth and validation semantics are unchanged: Self::admin(&env) is now bound to a local so it can be reused as the event actor, but it was already evaluated before .require_auth() when the call was inlined, so the evaluation and check order is identical. The publish happens after validation, auth and persistence, and only uses a Symbol topic plus a (u32, u32) / (i128, i128) data tuple, matching existing calls in this file, so it cannot panic. Refs LabsCrypt#1146
contracts/remittance_nft/src/test.rs defined test_transfer_rejects_burned_destination() twice, so the whole test target of this crate failed to compile and none of its unit tests could run: error[E0428]: the name `test_transfer_rejects_burned_destination` is defined multiple times The two definitions cover different code paths, so both are kept: - the auto-burn path (lowered burn threshold + record_default) is now test_transfer_rejects_auto_burned_destination - the explicit burn() path keeps test_transfer_rejects_burned_destination No test body was changed; the diff is just the name of the first one. The `contracts` CI job is currently a stub that only echoes "Contracts format, clippy, tests, and build passed", which is why this reached main. Refs LabsCrypt#1146
…sCrypt#1146) Adds four tests for the events emitted by the two config setters: - test_set_default_burn_threshold_emits_old_and_new_value: performs two real changes (3 -> 5, then 5 -> 9), asserting for each event the topics (DefaultBurnThresholdUpdated, admin) and the data (old, new), plus the persisted value. The second change proves `old` is read from storage rather than hard-coded. - test_set_min_repayment_amount_emits_old_and_new_value: same shape with an i128 (old_amount, new_amount) payload. - test_set_default_burn_threshold_rejected_value_emits_no_event: 0 and MAX_ALLOWED_BURN_THRESHOLD + 1 are rejected and publish nothing, which pins the "emit only after validation" ordering. - test_set_min_repayment_amount_requires_admin_auth: the setter is still admin-only. This setter previously had no test coverage at all. The tests assert topic equality against the expected Symbol/Address tuple and use env.events().all(), matching the existing event assertions for AdminTransferred and Mint in this file. Because the test host only keeps the most recent invocation's events, each capture happens immediately after the setter call, before any other contract call. cargo test -p remittance_nft --lib: 92 passed; 0 failed; 0 ignored (88 pre-existing + these 4). Refs LabsCrypt#1146
The `backend` CI job fails at its Lint step with 8 prettier/prettier errors, so its Build, Type check and Run tests steps never execute and the whole pipeline stays red. The errors are pre-existing (they come from files unrelated to the contracts change in this branch) but they block this PR from going green, so they are fixed here. `prettier --write` on exactly the four files eslint flagged: - src/__tests__/remittanceFilters.test.ts (1 error, line 42) - src/services/__tests__/auditLogService.pagination.test.ts (2 errors, lines 103/125) - src/services/auditLogService.ts (1 error, line 27) - src/tests/idempotency.namespace.test.ts (4 errors, lines 2/36/78/176) Formatting only: line-wrap decisions, nothing else. No logic, no import added or removed, no assertion touched. Verified with `prettier --check` on the same four files, which now reports "All matched files use Prettier code style!". Refs LabsCrypt#1146
The backend job has never reached its Run tests step: Lint failed first, so 9 broken assertions sat undetected on main. With lint fixed they fail, and every one of them is a test-side bug - each implementation is internally consistent with its documented behaviour: auditLogService.pagination.test.ts (LabsCrypt#1808) - 'pages with a (created_at, id) ...' called getAuditLogs with no cursor and then asserted the keyset predicate that only a cursor produces. - 'emits a composite nextCursor' split the cursor on the first ':' although the documented format is `${iso}:${id}` and an ISO timestamp contains ':'; it now splits on the last one, exactly as decodeCursor reads it back. - 'resumes correctly from a cursor it previously issued' inspected the first SELECT of the test instead of the resumed one; the pageQuery() helper now returns the most recent page query. - 'counts an unfiltered table as a single plain query' compared against a string without the trailing space the builder interpolates; it now asserts there is no WHERE clause and compares the trimmed statement. - 'keeps the documented filter surface' expected 8 filters, but both AuditLogFilters and the swagger docs expose 7; it now pins those 7 names. idempotency.test.ts (pre-LabsCrypt#1809 expectations) - expected `idemp:${key}` and `idemp:${key}:lock`; LabsCrypt#1809 namespaces both keys per wallet (this fixture is unauthenticated, hence the `anon` namespace). idempotency.namespace.test.ts (LabsCrypt#1809's own tests) - the cache and lock mocks answered identically for every key, which cannot model a keyed store: 'does not replay another wallet's cached response' handed Bob's entry to Alice, and 'does not reject a user with 409' hit Bob's lock. Both mocks are now key-aware and the assertions pin the namespaced key plus the fact that Alice's handler still runs. Verified locally: the three files pass under jest (28 tests) and `eslint .` reports 0 errors / 30 pre-existing warnings. No production code changes. Refs LabsCrypt#1808, LabsCrypt#1809
This was referenced Sep 29, 2026
…-nft-setter-events Resolves 5 conflicts: 3 backend test files + remittance_nft lib.rs/test.rs. backend (stale-test repairs): - auditLogService.pagination / idempotency / idempotency.namespace tests now take main's independently-authored fixes (LabsCrypt#1808/LabsCrypt#1809). This PR's own f261f16 (prettier) and 2fc4fc7 (stale-test repair) are superseded and are not reapplied. auditLogService.ts and remittanceFilters.test.ts auto-merged to main's version, which is byte-identical to this PR's prettier fix. remittance_nft/src/lib.rs: - set_min_repayment_amount keeps main's typed-error contract from LabsCrypt#1843 (-> Result<(), NftError>, rejects negatives with NftError::InvalidAmount) AND this PR's MinRepaymentUpdated (old,new) event (LabsCrypt#1146). - set_default_burn_threshold keeps this PR's DefaultBurnThresholdUpdated event and hoisted admin auth; main never touched this function. remittance_nft/src/test.rs: - main already deleted the duplicate test_transfer_rejects_burned_destination, so this PR's f73237c rename is redundant and is dropped (no rename is reapplied on top of main's already-unique name). - The 4 new LabsCrypt#1146 setter-event tests are preserved.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1146
What changed
set_default_burn_thresholdandset_min_repayment_amountmutate protocol-critical instance config and published nothing, so off-chain consumers (indexer, webhooks, audit trails) could not observe changes to two of the highest-stakes risk parameters. Every other admin action in this contract (authorize_minter,revoke_minter,pause,unpause, admin transfer,set_loan_manager) already emits an event.Both setters now capture the outgoing value before the overwrite and publish the full old -> new transition after the state write:
set_default_burn_threshold(Symbol::new(env, "DefaultBurnThresholdUpdated"), admin)(old_threshold: u32, new_threshold: u32)set_min_repayment_amount(Symbol::new(env, "MinRepaymentUpdated"), admin)(old_amount: i128, new_amount: i128)Why this exact shape:
contracts/loan_manager/src/events.rsdocuments the repo's admin config-update convention (("MinRepaymentUpdated", admin: Address)->(old_amount: i128, new_amount: i128)), andbackend/src/services/eventIndexer.tsalready decodes that shape for every event inADMIN_CONFIG_EVENT_TYPES(it readstopic[1]as the actor address and the second tuple element as the new value, e.g.// (type, admin), [old_amount, new_amount]). Reusing it means this event lands in the existing admin-config indexing/audit path with no indexer change at all, which is what the issue asked for ("so the indexer stays consistent"). Both names are longer than 9 chars, soSymbol::newis required (symbol_short!would not compile) - identical to the existingLoanMgrSet/AdminTransferredevents in this same file.Changed files
contracts/remittance_nft/src/lib.rs- event emission in the two setters (+40 / -2)contracts/remittance_nft/src/test.rs- 1-line test rename + 4 new tests (+157 / -1)backend/**- CI-unblocking only: 4 files reformatted withprettier --write(f261f16f) and 3 test files repaired (2fc4fc7a). No production behaviour changes; details in caveat 7.Done-when checklist from the issue
set_default_burn_thresholdemits an event with the old and new threshold ->test_set_default_burn_threshold_emits_old_and_new_valueperforms two real changes (3 -> 5, then5 -> 9), asserting topics(DefaultBurnThresholdUpdated, admin)and data(old, new)for both events plus the persisted value. The second change proves theoldvalue is read from storage, not hard-coded.set_min_repayment_amountemits an event with the old and new amount ->test_set_min_repayment_amount_emits_old_and_new_valuedoes the same for0 -> 1_000_000then1_000_000 -> 2_500_000with ani128payload.test_set_default_burn_threshold_rejected_value_emits_no_eventwhich pins that nothing is published when validation rejects the change (0 andMAX_ALLOWED_BURN_THRESHOLD + 1).cargo test,cargo fmt, clippy green; CI passes -> all 12 checks on this PR are green, includingbackend(Lint,Build,Type check,Run tests= 747 passed / 0 failed) and both CodeQL analyses. Getting there took two extra CI-unblocking commits for breakage that predates this branch;contractsitself is still only anecho(caveats 5 and 7), so the Rust command outputs below remain the real gate.Raw encoded event, taken from the regenerated
contracts/remittance_nft/test_snapshots/test/test_set_default_burn_threshold_upper_bound_valid.1.json(i.e. the existing test, which changed because it now emits):[{"event":{"ext":"v0","contract_id":"0000...0002","type_":"contract","body":{"v0":{"topics":[{"symbol":"DefaultBurnThresholdUpdated"},{"address":"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"}],"data":{"vec":[{"u32":3},{"u32":1000}]}}}},"failed_call":false}]topics = [DefaultBurnThresholdUpdated, admin],data = (old = 3 = DEFAULT_BURN_THRESHOLD, new = 1000).Verification - exact commands and real output
Ran from
contracts/(whererust-toolchain.tomlpins1.85.0):88 of those are pre-existing tests (
test_set_default_burn_threshold_upper_bound_valid,..._invalid,test_record_default_auto_burns_after_threshold,test_transfer_rejects_burned_destination, ...) and they still pass unchanged - the event is purely additive. The 4 new ones:Baseline for comparison - the same command on a tree whose only difference from
mainis the test rename (no production change at all), which is what makes the "88 + 4 = 92" comparison meaningful:Caveats I hit - all pre-existing on
main, none caused by this PRcontracts/remittance_nft/src/test.rsdid not compile onmain. It definedtest_transfer_rejects_burned_destination()twice (lines 1230 and 1327), socargo testfor this crate failed witherror[E0428]: the name ... is defined multiple timesand none of its unit tests could run. Both definitions are real tests covering different paths (auto-burn via threshold +record_defaultvs. explicitburn()), so I kept both and renamed the first totest_transfer_rejects_auto_burned_destination(1-line change, separate commitf73237c9). This is required for the issue's own "tests assert the events publish" item to be verifiable at all, and it is why the baseline above is 88 tests. ThecontractsCI job does not compile anything today (see caveat 5), which is how this reachedmain; making that job real would be a worthwhile follow-up.Plain
cargo test -p remittance_nftcannot link in this Windows environment. Because the crate iscrate-type = ["cdylib", "rlib"]and test builds pull in thetestutilshost dependencies, the test harness is linked as a dll and GNU ld aborts withld: error: export ordinal too large: 68831. I verified this is environmental, not related to this change: on a tree whose only delta vsmainis the one-line rename above,cargo test -p loan_manager --no-runfails exactly the same way forremittance_nft,lending_poolandloan_manager:cargo test -p remittance_nft --lib(what I ran) builds only the lib test harness and works fine, so the full unit suite really is exercised.cargo fmt --all -- --checkreports pre-existing drift incontracts/lending_pool/src/lib.rs:686,699andcontracts/loan_manager/src/lib.rs:1792,contracts/loan_manager/src/test.rs:5774(line-wrap style plus a trailing newline). Those files are byte-identical tomainin this branch (git diff upstream/main -- contracts/lending_pool contracts/loan_manageris empty), so this PR does not introduce it;cargo fmt -p remittance_nft -- --checkis clean.cargo clippy --workspace --all-targets -- -D warningscannot compileloan_manager's test target onmaineither - pre-existing, in files untouched here:The
remittance_nftpackage itself is clippy-clean with-D warnings(output above).The CI
contractsjob is a stub:.github/workflows/ci.ymlonly runsecho "Contracts format, clippy, tests, and build passed", so CI is green for this PR but could not have caught caveats 1 or 4. The Rust commands above are therefore the real gate.Snapshot churn deliberately excluded.
.gitignoreline 5 listscontracts/**/test_snapshots/, but 46 snapshot files undercontracts/remittance_nft/test_snapshots/are nevertheless tracked (an ignore rule does not apply to already-tracked files), and because these tests capture snapshots viato_test_snapshot_fileinstead of asserting them, running the suite rewrites 7 of those tracked files. The same 7 files drift onmainas well - the rename-only baseline rewrites the identical 7 with zero functional change - so none of that churn is in this diff, even though the event evidence above does come from one of those regenerated files.The
backendjob was red for reasons unrelated to this PR, and is now fixed in it. Two independent pre-existing breakages were hiding behind each other, because the job aborts at the first failing step:a.
Lintfailed with 8prettier/prettiererrors (plus 30 warnings) inbackend/src/services/auditLogService.ts,backend/src/services/__tests__/auditLogService.pagination.test.ts,backend/src/__tests__/remittanceFilters.test.tsandbackend/src/tests/idempotency.namespace.test.ts. Those files are byte-identical tomainon this branch and I reproduced the failure locally. Fixed inf261f16fby runningprettier --writeon exactly those four files (line-wrap and trailing-comma normalisation only).eslint .now reports 0 errors / 30 pre-existing warnings, soLint,BuildandType checkpass.b.
Run teststhen failed, because that step had never executed in CI:Tests: 9 failed, 33 skipped, 738 passed, 780 total. All 9 were test-side bugs - every implementation matches its own documented contract - and are fixed in2fc4fc7a:auditLogService.pagination.test.ts(5): asserted the keyset predicate while never passing a cursor; split the documented${iso}:${id}cursor on the first:although ISO timestamps contain:; read the firstSELECTof the test instead of the resumed one; compared the count SQL against a string missing the builder's trailing space; expected 8 filter keys where bothAuditLogFiltersandsrc/swagger/adminSwagger.tsdocument 7.idempotency.test.ts(2): still expected the pre-[Security][Backend] idempotencyMiddleware uses raw un-namespaced Idempotency-Key allowing cross-user replay and denial-of-service #1809 un-namespacedidemp:<key>/idemp:<key>:lock.idempotency.namespace.test.ts(2): the cache/lock mocks answered identically for every key, so Alice was handed Bob's cached body and hit Bob's lock; both mocks are now key-aware.Result on the same commit:
Test Suites: 5 skipped, 101 passed, 101 of 106 total/Tests: 33 skipped, 747 passed, 780 total, and all 12 checks are green (backendin 1m19s, plusAnalyze (rust)andAnalyze (javascript-typescript)). Both commits are format/test-only, so nothing about [Contracts] RemittanceNFT parameter setters emit no events (set_default_burn_threshold, set_min_repayment_amount) #1146's behaviour changes - happy to split them out if you would rather review them separately.Out of scope, intentionally left untouched
migrate()/upgrade().backend/changes are the prettier reformat and the test repairs inf261f16f/2fc4fc7a(caveat 7); no production logic changed.require_auth(), then the pause check;Self::admin(&env)is only bound to a local so the same already-read address can be reused as the event actor (it was already evaluated before.require_auth()when the call was inlined). The negative-amount panic inset_min_repayment_amountis unchanged, and the newtest_set_min_repayment_amount_requires_admin_authtest documents that admin-only enforcement still holds. Note that this setter previously had no test coverage of any kind.Symboltopic plus a fixed(u32, u32)/(i128, i128)data tuple, exactly like the existingAdminTransferred/MinScoreUpdated-style calls in this repo.Commits
c2f180c9fix(remittance_nft): emit parameter-change events from the two config setters ([Contracts] RemittanceNFT parameter setters emit no events (set_default_burn_threshold, set_min_repayment_amount) #1146)f73237c9fix(remittance_nft): rename duplicate test that breaks the test targetcebaef82test(remittance_nft): assert old/new values in the setter events ([Contracts] RemittanceNFT parameter setters emit no events (set_default_burn_threshold, set_min_repayment_amount) #1146)f261f16fstyle(backend): apply the prettier formatting the lint job requires2fc4fc7atest(backend): repair the 9 stale tests that keep the backend job red