-
Notifications
You must be signed in to change notification settings - Fork 67
Add test for SMT root replay (#211) #236
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
Open
jaymhorsh
wants to merge
5
commits into
Alien-Protocol:main
Choose a base branch
from
jaymhorsh:test/core-smt-root-replay-protection
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8a43bd4
feat(escrow-contract): escrow_contract - implement deposit(commitment…
jaymhorsh 87974a4
chore(sdk): add Jest configuration and CI workflow for SDK unit tests
jaymhorsh dffca31
test(core-contract): add test for SMT root replay - confirm second ca…
jaymhorsh d295cdc
Merge branch 'main' into test/core-smt-root-replay-protection
jaymhorsh 9216382
Merge branch 'main' into test/core-smt-root-replay-protection
jaymhorsh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| name: SDK Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: ["main"] | ||
| pull_request: | ||
| branches: ["main"] | ||
|
|
||
| jobs: | ||
| sdk-tests: | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: zk/sdk | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
|
|
||
| - name: Install dependencies | ||
| run: npm install | ||
|
|
||
| - name: Run SDK tests | ||
| run: npm test |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,329 @@ | ||
| # Auction Contract Specification | ||
|
|
||
| The Auction contract implements two auction flows that coexist in the same contract: | ||
|
|
||
| - A singleton username auction flow keyed by instance storage (`close_auction`, `claim_username`). | ||
| - An ID-indexed auction flow keyed by persistent storage (`create_auction`, `place_bid`, `close_auction_by_id`, `claim`). | ||
|
|
||
| Both flows use Soroban auth (`require_auth`) and ledger timestamp checks to enforce access and timing constraints. | ||
|
|
||
| ## Public Entry Points | ||
|
|
||
| ### Function: `create_auction` | ||
|
|
||
| Creates a new auction identified by `id`. | ||
|
|
||
| #### Interface | ||
|
|
||
| ```rust | ||
| pub fn create_auction( | ||
| env: Env, | ||
| id: u32, | ||
| seller: Address, | ||
| asset: Address, | ||
| min_bid: i128, | ||
| end_time: u64, | ||
| ) | ||
| ``` | ||
|
|
||
| #### Authorization | ||
|
|
||
| - `seller.require_auth()` must succeed. | ||
|
|
||
| #### Requirements & Validation | ||
|
|
||
| - Auction ID must not already exist (`storage::auction_exists(&env, id) == false`). | ||
| - If ID already exists, function aborts with `AuctionError::AuctionNotOpen`. | ||
|
|
||
| #### State Transitions | ||
|
|
||
| - Writes `seller` to `AuctionKey::Seller(id)`. | ||
| - Writes bidding token `asset` to `AuctionKey::Asset(id)`. | ||
| - Writes `min_bid` to `AuctionKey::MinBid(id)`. | ||
| - Writes `end_time` to `AuctionKey::EndTime(id)`. | ||
| - Sets status to `AuctionStatus::Open` in `AuctionKey::Status(id)`. | ||
|
|
||
| #### Events Emitted | ||
|
|
||
| - None in current implementation. | ||
|
|
||
| #### Errors | ||
|
|
||
| - `AuctionError::AuctionNotOpen` when `id` already exists. | ||
| - Host auth failure if `seller` does not authorize. | ||
|
|
||
| #### Edge Cases | ||
|
|
||
| - **Duplicate auction ID**: explicitly rejected (panic with `AuctionNotOpen`). | ||
| - **No min/end validation**: contract currently does not enforce `min_bid > 0` or `end_time > now` at creation. | ||
|
|
||
| ### Function: `place_bid` | ||
|
|
||
| Places a bid on an existing auction and refunds the previously highest bidder. | ||
|
|
||
| #### Interface | ||
|
|
||
| ```rust | ||
| pub fn place_bid(env: Env, id: u32, bidder: Address, amount: i128) | ||
| ``` | ||
|
|
||
| #### Authorization | ||
|
|
||
| - `bidder.require_auth()` must succeed. | ||
|
|
||
| #### Requirements & Validation | ||
|
|
||
| - Auction must still be open by time: `env.ledger().timestamp() < auction_end_time`. | ||
| - Bid must satisfy both: | ||
| - `amount >= min_bid` | ||
| - `amount > highest_bid` | ||
|
|
||
| If timing check fails, function aborts with `AuctionError::AuctionNotOpen`. | ||
| If bid floor/outbid check fails, function aborts with `AuctionError::BidTooLow`. | ||
|
|
||
| #### State Transitions | ||
|
|
||
| 1. Transfers `amount` of auction asset token from `bidder` to contract. | ||
| 2. If previous highest bidder exists, transfers prior `highest_bid` from contract back to that bidder. | ||
| 3. Updates `AuctionKey::HighestBidder(id)` to current `bidder`. | ||
| 4. Updates `AuctionKey::HighestBid(id)` to `amount`. | ||
|
|
||
| #### Events Emitted | ||
|
|
||
| - None in current implementation. | ||
|
|
||
| #### Errors | ||
|
|
||
| - `AuctionError::AuctionNotOpen` when auction time window is closed. | ||
| - `AuctionError::BidTooLow` when bid is below min or not strictly above current highest. | ||
| - Host auth failure if `bidder` does not authorize. | ||
| - Token transfer failure if token contract transfer preconditions are not met. | ||
|
|
||
| #### Edge Cases | ||
|
|
||
| - **Zero-bid history**: first bid is accepted if it meets `min_bid` and auction is still open. | ||
| - **Equal-to-highest bid**: rejected (`amount <= highest_bid` path). | ||
| - **Late bid at exact end timestamp**: rejected because condition is `timestamp >= end_time`. | ||
|
|
||
| ### Function: `close_auction_by_id` | ||
|
|
||
| Closes an ID-indexed auction once its end time has passed. | ||
|
|
||
| #### Interface | ||
|
|
||
| ```rust | ||
| pub fn close_auction_by_id(env: Env, id: u32) | ||
| ``` | ||
|
|
||
| #### Authorization | ||
|
|
||
| - No explicit caller auth in current implementation. | ||
|
|
||
| #### Requirements & Validation | ||
|
|
||
| - Current ledger timestamp must be at least the auction end time. | ||
| - If `timestamp < end_time`, function aborts with `AuctionError::AuctionNotClosed`. | ||
|
|
||
| #### State Transitions | ||
|
|
||
| - Sets `AuctionKey::Status(id)` to `AuctionStatus::Closed`. | ||
|
|
||
| #### Events Emitted | ||
|
|
||
| - None in current implementation. | ||
|
|
||
| #### Errors | ||
|
|
||
| - `AuctionError::AuctionNotClosed` when called before end time. | ||
|
|
||
| #### Edge Cases | ||
|
|
||
| - **Early close attempt**: rejected with `AuctionNotClosed`. | ||
| - **No bids placed**: still closes successfully; later claim semantics determine payout/ownership behavior. | ||
|
|
||
| ### Function: `close_auction` | ||
|
|
||
| Closes the singleton username auction flow and emits closure metadata. | ||
|
|
||
| #### Interface | ||
|
|
||
| ```rust | ||
| pub fn close_auction( | ||
| env: Env, | ||
| username_hash: BytesN<32>, | ||
| ) -> Result<(), AuctionError> | ||
| ``` | ||
|
|
||
| #### Authorization | ||
|
|
||
| - No explicit caller auth in current implementation. | ||
|
|
||
| #### Requirements & Validation | ||
|
|
||
| - Current instance `status` must be `AuctionStatus::Open`. | ||
| - Current ledger timestamp must be at least instance `end_time`. | ||
|
|
||
| Returns: | ||
|
|
||
| - `Err(AuctionError::AuctionNotOpen)` if status is not `Open`. | ||
| - `Err(AuctionError::AuctionNotClosed)` if called before end time. | ||
|
|
||
| #### State Transitions | ||
|
|
||
| - Sets instance `DataKey::Status` to `AuctionStatus::Closed`. | ||
| - Reads instance `DataKey::HighestBidder` and `DataKey::HighestBid` for event payload. | ||
|
|
||
| #### Events Emitted | ||
|
|
||
| - Emits `AuctionClosedEvent` via `emit_auction_closed` with: | ||
| - `username_hash` | ||
| - `winner: Option<Address>` | ||
| - `winning_bid: u128` | ||
|
|
||
| #### Errors | ||
|
|
||
| - `AuctionError::AuctionNotOpen` | ||
| - `AuctionError::AuctionNotClosed` | ||
|
|
||
| #### Edge Cases | ||
|
|
||
| - **Zero bids**: event emits `winner = None`, `winning_bid = 0`. | ||
| - **Repeated close**: second close call fails with `AuctionNotOpen` because status is no longer `Open`. | ||
|
|
||
| ### Function: `claim_username` | ||
|
|
||
| Allows winner of singleton username auction to deploy/claim the username via factory contract. | ||
|
|
||
| #### Interface | ||
|
|
||
| ```rust | ||
| pub fn claim_username( | ||
| env: Env, | ||
| username_hash: BytesN<32>, | ||
| claimer: Address, | ||
| ) -> Result<(), AuctionError> | ||
| ``` | ||
|
|
||
| #### Authorization | ||
|
|
||
| - `claimer.require_auth()` must succeed. | ||
|
|
||
| #### Requirements & Validation | ||
|
|
||
| - Instance status must not already be `Claimed`. | ||
| - Instance status must be `Closed`. | ||
| - `claimer` must equal stored highest bidder. | ||
| - Factory contract address must exist in `DataKey::FactoryContract`. | ||
|
|
||
| Returns: | ||
|
|
||
| - `Err(AuctionError::AlreadyClaimed)` if already claimed. | ||
| - `Err(AuctionError::NotClosed)` if not closed. | ||
| - `Err(AuctionError::NotWinner)` if caller is not winner. | ||
| - `Err(AuctionError::NoFactoryContract)` if factory address is missing. | ||
|
|
||
| #### State Transitions | ||
|
|
||
| 1. Sets instance `DataKey::Status` to `AuctionStatus::Claimed`. | ||
| 2. Invokes factory contract method `deploy_username(username_hash, claimer)`. | ||
|
|
||
| #### Events Emitted | ||
|
|
||
| - Emits `UsernameClaimedEvent` via `emit_username_claimed` with: | ||
| - `username_hash` | ||
| - `claimer` | ||
|
|
||
| #### Errors | ||
|
|
||
| - `AuctionError::AlreadyClaimed` | ||
| - `AuctionError::NotClosed` | ||
| - `AuctionError::NotWinner` | ||
| - `AuctionError::NoFactoryContract` | ||
| - Host auth failure if `claimer` does not authorize. | ||
|
|
||
| #### Edge Cases | ||
|
|
||
| - **No bids**: no highest bidder exists, so claim fails with `NotWinner`. | ||
| - **Claim race**: first valid claim sets status to `Claimed`; subsequent claims fail with `AlreadyClaimed`. | ||
|
|
||
| ### Function: `claim` | ||
|
|
||
| Finalizes an ID-indexed auction by allowing the winner to release funds to seller. | ||
|
|
||
| #### Interface | ||
|
|
||
| ```rust | ||
| pub fn claim(env: Env, id: u32, claimant: Address) | ||
| ``` | ||
|
|
||
| #### Authorization | ||
|
|
||
| - `claimant.require_auth()` must succeed. | ||
|
|
||
| #### Requirements & Validation | ||
|
|
||
| - Auction status for `id` must be `AuctionStatus::Closed`. | ||
| - Auction must not already be claimed (`auction_is_claimed == false`). | ||
| - `claimant` must equal current highest bidder. | ||
|
|
||
| Function aborts with: | ||
|
|
||
| - `AuctionError::NotClosed` when status is not closed. | ||
| - `AuctionError::AlreadyClaimed` when already claimed. | ||
| - `AuctionError::NotWinner` when claimant is not highest bidder. | ||
|
|
||
| #### State Transitions | ||
|
|
||
| 1. Reads token `asset`, `winning_bid`, and `seller` for auction `id`. | ||
| 2. Transfers `winning_bid` from contract to `seller`. | ||
| 3. Marks `AuctionKey::Claimed(id)` as `true`. | ||
|
|
||
| #### Events Emitted | ||
|
|
||
| - None in current implementation. | ||
|
|
||
| #### Errors | ||
|
|
||
| - `AuctionError::NotClosed` | ||
| - `AuctionError::AlreadyClaimed` | ||
| - `AuctionError::NotWinner` | ||
| - Host auth failure if `claimant` does not authorize. | ||
| - Token transfer failure if transfer cannot be completed. | ||
|
|
||
| #### Edge Cases | ||
|
|
||
| - **No bids**: highest bidder is `None`; all claim attempts fail with `NotWinner`. | ||
| - **Double claim**: second successful claimant attempt is blocked by `AlreadyClaimed`. | ||
|
|
||
| ## Error Variants (Contract-Wide) | ||
|
|
||
| Defined in `errors.rs`: | ||
|
|
||
| - `NotWinner` | ||
| - `AlreadyClaimed` | ||
| - `NotClosed` | ||
| - `NoFactoryContract` | ||
| - `Unauthorized` | ||
| - `InvalidState` | ||
| - `BidTooLow` | ||
| - `AuctionNotOpen` | ||
| - `AuctionNotClosed` | ||
|
|
||
| Note: `Unauthorized` and `InvalidState` are defined but not currently emitted by the public entry points above; authorization failures are enforced primarily through host-level `require_auth`. | ||
|
|
||
| ## Event Types (Contract-Wide) | ||
|
|
||
| Defined in `events.rs`: | ||
|
|
||
| - `AuctionCreatedEvent` | ||
| - `BidPlacedEvent` | ||
| - `AuctionClosedEvent` | ||
| - `UsernameClaimedEvent` | ||
| - `BidRefundedEvent` | ||
|
|
||
| Current emission in public entry points: | ||
|
|
||
| - `close_auction` emits `AuctionClosedEvent`. | ||
| - `claim_username` emits `UsernameClaimedEvent`. | ||
| - Other listed entry points currently emit no events. | ||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Core Contract Notes | ||
|
|
||
| ## SMT Root Sequencing Requirement | ||
|
|
||
| `register_resolver` enforces strict root sequencing: | ||
|
|
||
| - `public_signals.old_root` must exactly equal the current on-chain SMT root. | ||
| - A successful `register_resolver` updates the on-chain root to `public_signals.new_root`. | ||
| - Any later call reusing the pre-update root is rejected as stale. | ||
|
|
||
| This replay protection prevents re-submitting proofs against an already-consumed root. In tests, the stale replay path is asserted to panic with `Error(Contract, #4)` (`StaleRoot`). | ||
|
|
Oops, something went wrong.
Oops, something went wrong.
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.
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.
PR scope appears misaligned with the stated objective (SMT stale-root replay test).
This change set documents the auction contract, but the objective for issue
#211requires addingtest_register_resolver_stale_root_after_first_registrationand documenting sequencing ingateway-contract/contracts/core_contract/core.md. As-is, the acceptance criteria look unmet in this PR payload.🧰 Tools
🪛 LanguageTool
[style] ~42-~42: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...n
assettoAuctionKey::Asset(id). - Writesmin_bidtoAuctionKey::MinBid(id). ...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~43-~43: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...min_bid
toAuctionKey::MinBid(id). - Writesend_timetoAuctionKey::EndTime(id)`...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~222-~222: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...losed. -
Err(AuctionError::NotWinner)if caller is not winner. - `Err(AuctionErr...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~223-~223: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...
Err(AuctionError::NoFactoryContract)if factory address is missing. #### State...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~273-~273: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...dy claimed. -
AuctionError::NotWinnerwhen claimant is not highest bidder. #### S...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🤖 Prompt for AI Agents