fix: reject credential transfer with a typed error, plus several build-blocking bug fixes - #378
Merged
DeFiVC merged 7 commits intoAug 31, 2026
Conversation
iduhtheman
force-pushed
the
fix/credentials-versioning-batch-iduhtheman
branch
from
August 30, 2026 19:25
c3489f3 to
78365ab
Compare
|
@iduhtheman Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…ubled reference complete_module_in_place and submit_quiz_score_in_place both take progress: &mut ProgressInfo, but the single storage write at the end of each passed &progress -- taking a reference to the reference (&&mut ProgressInfo) instead of the reference itself, which does not satisfy the IntoVal bound Persistent::set requires and fails to compile. Each function then also recomputed overall_progress and eligible_for_credential a second time immediately after the write, overwriting nothing (the write already happened) and never being persisted -- dead code left over from a merge that inserted the version-tracking write in front of, instead of in place of, the original recompute-then-let-the-caller-write pattern. Fixed both call sites to pass the &mut ProgressInfo directly (no extra &) and removed the now-pointless post-write recompute in each -- the values already written to storage are the correct, final ones computed earlier in the same function, and nothing downstream reads the recomputed-and-discarded copies.
…CredentialDisplay> CredentialVerification.display was Option<CredentialDisplay>, but soroban-sdk 21.7.7's #[contracttype] derive does not implement the ScVal (client/spec) conversion for Option<T> where T is a custom struct -- only for SDK built-ins like Symbol. This compiled under a bare cargo check (which only exercises the runtime Val path used inside the contract itself) but failed cargo test / the generated client with a concrete E0277 trait-bound error on TryFrom<&Option<CredentialDisplay>> for ScVal, confirmed directly against this SDK version -- a real, previously-undetected break in the already-merged code, and there was no existing test coverage exercising verify_credential_with_display at all to have caught it. Fixed by making display a Vec<CredentialDisplay> holding 0 or 1 elements instead, via new no_display/one_display helpers -- every field inside CredentialDisplay itself stays a true Option<Symbol>, which does work, so nothing about the type's actual optionality is weakened. Added two tests: no display data set (empty Vec, info unaffected) and display data set and returned correctly.
Course gained a version: u32 field (ChainLearnOfficial#245); this test's manually- constructed Course literal, used to bypass create_course's own validation for a zero-module edge case, was never updated and failed to compile against the new struct shape.
test_renew_credential_extends_expiry referenced .expiry, but CredentialInfo's actual field is expires_at -- a naming mismatch between this test and the struct it exercises that left the test suite unable to compile.
…erged ChainLearnOfficial#242's Soulbound transfer rejection This branch originally set out to implement ChainLearnOfficial#227 ("Add credential transfer rejection with reason") by making `transfer` return Result<(), ContractError> with a Soulbound variant and requiring `from.require_auth()`. While rebasing onto current main, it turned out ChainLearnOfficial#227 is a content-duplicate of ChainLearnOfficial#242 (identical title and body), which was already implemented and merged via ChainLearnOfficial#374. Upstream's version returns the same typed Soulbound error but deliberately omits `require_auth()`, since the rejection is unconditional and reads/writes no storage -- there is nothing to authorize. Equivalent tests already exist in tests/unit/credential_tests.rs (test_transfer_always_returns_soulbound_error, test_transfer_rejects_even_without_auth_or_existing_credential, test_transfer_does_not_mutate_credential_state), including one that explicitly asserts the call succeeds without any mocked auth. Kept upstream's implementation and doc comment as-is (citing ChainLearnOfficial#242) and added a note cross-referencing the ChainLearnOfficial#227 duplicate; did not reintroduce the auth requirement or duplicate the existing test coverage.
test_security_batch_claim_reward_supply_overflow_skips_without_panicking and test_governance_proposal_lifecycle were merged into the same file without a closing brace between them, leaving the first test's body open and swallowing the #[test] attribute meant for the second -- an unclosed-delimiter error blocking the entire test binary from compiling. Pre-existing on upstream/main since ChainLearnOfficial#379.
…nLearnOfficial#374's merge, and fix two resulting compile errors ChainLearnOfficial#374's merge of storage-size tracking (ChainLearnOfficial#239) into this file botched the surrounding hunk: it closed `ProgressTrackerDataKey`'s enum body right after the new `StorageSize` variant, leaving the pre-existing `Achievements`/`AchievementEarned` variants stranded as dangling tokens after `write_entry`'s function body instead of inside the enum, and left `write_entry` itself unclosed. This broke `cargo check --workspace` on plain `main` with a parse error, confirmed present identically on `main` before this branch touched anything -- `types.rs` was otherwise byte-for-byte unchanged from `main`. Moved `Achievements`/`AchievementEarned` back into the enum body (right after `StorageSize`, where the diff put them originally) and closed `write_entry` where the parser actually needed it. Fixing the parse error surfaced two further pre-existing, unrelated compile errors in the same achievement-awarding code path, also present unchanged on `main`: - `complete_module_in_place` called a `get_learner_stats_internal` that was never defined (only the public `get_learner_stats(env: Env, ...)` exists) -- likely a rename that was never finished. Called the real function instead, cloning `env`/`learner` since this call site only has `&Env`/`&Address`. - `earn_achievement`'s `achievement_earned` event published `&AchievementType` by reference; soroban-sdk 21.7.7 doesn't implement the ScVal conversion for a reference to a custom `#[contracttype]` enum (only for owned values and SDK built-ins), so publishing failed to compile. `achievement_type` isn't used after this call, so passed it by value instead. `cargo check --workspace` and `cargo test -p progress-tracker --lib` (79 passed) now succeed.
iduhtheman
force-pushed
the
fix/credentials-versioning-batch-iduhtheman
branch
from
August 31, 2026 08:44
78365ab to
458114b
Compare
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.
Summary
Originally scoped to #227, #228, #229, #230. While preparing this PR, issue number 245, 244, and 243 were found to be literal duplicates of #230, #229, and #228 respectively (identical titles), and another contributor's PR (issue-worker-linked PR #376) already implemented and merged that work first. This PR now covers #227 only — the one issue with no overlapping upstream work — plus several pre-existing, unrelated bugs discovered and fixed while getting this branch to actually build and test against the current
main.closes #227
Re-verified independently while rebasing onto current
main(2026-08-31): confirmed viagh issue viewthat #228, #229, #230 are stillOPENand still assigned to iduhtheman, and that #243, #244, #245 areCLOSEDwith byte-identical titles ("Add credential metadata update" / "Add credential display properties" / "Add course versioning"). Confirmed PR #376 isMERGEDand its body explicitly statescloses #246/#245/#244/#243with a description matching each issue's scope (Course structversionfield +update_course_version,CredentialDisplay/CredentialVerification/set_credential_display/get_credential_display,update_credential_metadata), all of which are present onmaintoday. The "duplicate" claim in this PR body is true — #228/#229/#230 are content-duplicates of #243/#244/#245 and are already fully implemented onmainvia #376. They remain open only because #376 closed them by their own (different) issue numbers rather than #228/#229/#230's; that's a bookkeeping cleanup for a maintainer to do (closing #228/#229/#230 as duplicates), not new code for this PR. No code changes were made for #228/#229/#230.The one feature in this PR
#227 — Credential transfer rejection with reason:
transferpreviously panicked with a plain string. It now returnsResult<(), ContractError>and rejects with a newSoulboundvariant instead, so callers can match on a typed error rather than parsing a panic message.Update while rebasing onto current
main: #227 turned out to itself be a content-duplicate of #242 (identical title/body to #227), which was independently implemented and merged first via #374. Upstream's version returns the same typedSoulbounderror but deliberately omitsfrom.require_auth()— the rejection is unconditional and reads/writes no storage, so (per #374's own design notes) there is nothing to authorize. Equivalent test coverage already exists intests/unit/credential_tests.rs(test_transfer_always_returns_soulbound_error,test_transfer_rejects_even_without_auth_or_existing_credential— which explicitly asserts the call succeeds with no mocked auth at all — andtest_transfer_does_not_mutate_credential_state). Kept upstream's implementation and doc comment as-is and added a cross-reference note to the #227 duplicate; did not reintroduce the auth requirement or duplicate the existing test coverage.Pre-existing bugs fixed as prerequisites (not part of #227's own scope)
Getting this branch to build and test against current
mainrequired fixing six separate, unrelated pre-existing breaks — each is its own isolated commit, none folded into the #227 feature commit:learn-token/storage.rs— a duplicate closing brace afterappend_claim_recordbroke parsing for the entirelearn-tokencrate (and therefore the whole workspace, since root-level integration tests depend on every workspace member). From learn-token: storage size tracking, plus edge-case coverage for per-address minting, claim history, and pause #373.progress-tracker/lib.rs—complete_module_in_placeandsubmit_quiz_score_in_placeboth takeprogress: &mut ProgressInfo, but the single storage write in each passed&progress(a reference to the reference) instead ofprogressdirectly, failing to compile. Each also recomputedoverall_progress/eligible_for_credentiala second time immediately after the write, which was silently discarded — dead code left over from a merge that inserted a new write in front of, instead of in place of, the original recompute-then-write pattern. From feat(progress-tracker): versioning, batch operations, and progress delegation #372 merging awkwardly against feat: add course versioning, credential display properties, and metadata update (#246, #245, #244, #243) #376.credential-nft'sCredentialVerification.display— declared asOption<CredentialDisplay>, butsoroban-sdk21.7.7's#[contracttype]derive does not implement theScVal(client/spec) conversion forOption<T>whereTis a custom struct — only for SDK built-ins likeSymbol. This compiled under a barecargo check(which only exercises the runtimeValpath) but failedcargo test/the generated client with a concreteE0277trait-bound error, confirmed directly against this SDK version — a real, previously-undetected break in the already-merged . Add credential display properties #244/feat: add course versioning, credential display properties, and metadata update (#246, #245, #244, #243) #376 code, since nothing exercisedverify_credential_with_displaybefore now. Fixed by using aVec<CredentialDisplay>holding 0 or 1 elements instead (every field insideCredentialDisplaystays a trueOption<Symbol>, which does work). Added 2 tests that were previously entirely missing for this function.tests/unit/progress_tests.rs— a manually-constructedCoursetest fixture was missing theversion: u32field . Add course versioning #245 added, failing to compile.tests/unit/credential_tests.rs—test_renew_credential_extends_expiryreferenced a.expiryfield that doesn't exist onCredentialInfo(the real field isexpires_at), failing to compile.tests/unit/token_tests.rs— two adjacent test functions (test_security_batch_claim_reward_supply_overflow_skips_without_panickingandtest_governance_proposal_lifecycle) were merged together without a closing brace between them, an unclosed-delimiter error blocking the entiretoken_testsbinary. From test(learn-token): add security tests for overflow/underflow protection #379.progress-tracker/types.rs(found while rebasing onto currentmain) — Soulbound transfer rejection, delayed admin transfer, init check, and storage-size tracking #374's merge of storage-size tracking (. Add storage size tracking #239) botched the surrounding hunk: it closedProgressTrackerDataKey's enum body right after the newStorageSizevariant, stranding the pre-existingAchievements/AchievementEarnedvariants as dangling tokens afterwrite_entry's function body (outside any enum) and leavingwrite_entryitself unclosed — a parse error blockingcargo check --workspaceon plainmain. Restored the two variants to the enum body and closedwrite_entryproperly. Fixing the parse error surfaced two further pre-existing compile errors in the same code path (also unchanged onmain):complete_module_in_placecalled an undefinedget_learner_stats_internalinstead of the real publicget_learner_stats, andearn_achievement's event publish passed&AchievementTypeby reference, which soroban-sdk 21.7.7 can't convert (only owned custom-enum values are supported). Fixed both minimally in place.Test plan
cargo check --workspace— passes cleanly.cargo test -p progress-tracker --lib(79 passed),cargo test -p credential-nft --lib(37 passed).cargo test --test credential_tests --test progress_tests --test token_tests --test credential_flow --test full_flow --test batch_operations --test admin_role_flow— all pass (27 + 97 + 50 + 6 + 1 + 3 + 3).cargo test --test security_arithmetic_tests --test security_reentrancy_tests --test token_flow --test security_auth_tests --test xcontract_call_tests— all pass.cargo fmtrun on every file this PR touches.cargo clippy -p progress-tracker -p credential-nft— no errors, only pre-existing needless-borrow style warnings on lines this PR doesn't touch.Pre-existing, unrelated bugs found and deliberately left unfixed (disclosed, not fixed — out of scope for this PR)
learn-token's owntest_storage_size_starts_at_zerofails (expects0, gets1) — once the crate can actually compile (fix Implement unit tests for learn-token contract (tests/unit/token_tests.rs) #1 above unblocks it for the first time), this test genuinely fails. This is a logic bug in learn-token: storage size tracking, plus edge-case coverage for per-address minting, claim history, and pause #373's own storage-size-tracking feature (initialize()appears to create one tracked entry the test doesn't expect), unrelated to . Add credential transfer rejection with reason #227 or any of the compile-blocking fixes above.tests/integration/upgrade_tests.rs::test_contract_upgrade_preserves_state_and_updates_versionfails at runtime withHostError: Error(Storage, MissingValue)insidelearn_token'supgradecall. This file is byte-for-byte identical to plainmain, and this branch makes no changes tolearn-token's upgrade mechanism at all — confirmed pre-existing and unrelated.tests/integration/emergency_pause_flow.rs,tests/unit/event_emission_tests.rs,tests/integration/content_hash_flow.rs,tests/integration/multi_admin_flow.rs,tests/integration/security_double_spending_tests.rs,tests/integration/token_vesting_flow.rsall fail to compile on plainmain(missinguse soroban_sdk::IntoVal;/use soroban_sdk::testutils::Ledger;imports, and aVec<Val>/Valtype mismatch inevent_emission_tests.rs). All six files are byte-for-byte identical tomain— none of this branch's commits touch them. Left unfixed as out of scope; flagging since they block a fullcargo test --workspacefrom ever succeeding onmaintoday, regardless of this PR.