Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/contracts-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

# scripts/release-notes.sh needs only bash, git, awk and sed, and its
# paired test builds its own throwaway repository, so it sits outside the
# harness guard below and runs before the CDT install. Every ref that
# carries this step also carries the script, so the step is unconditional.
- name: Run release-notes composer test
run: bash scripts/release-notes.test.sh

- name: Check for test harness
id: harness
run: |
Expand Down
17 changes: 17 additions & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Authors and credits

AtomicMarket is part of the AtomicAssets NFT ecosystem, created by
[Pink Network](https://pink.gg). Stewardship of AtomicMarket and the related
software passed from Pink Network to Spielworks, and then to FACINGS, the
current owner and maintainer. The v1 contract was written by
**Jona Wilmsmann (Pink Network)**.

## AtomicMarket v2

Most of the v2 contract was authored by
**t-break ([@on-a-t-break](https://github.com/on-a-t-break))**, and reached
this repository through the `on-a-t-break/v2/integration-2` branch, whose
commits carry that authorship.

The v2 review, the hardening changes, the XPR port, the VeRT test migration and
the releases are the work of the FACINGS team.
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Changelog

Notable changes to the AtomicMarket contract. This file starts at 2.0.0; the
releases published before it live in
[GitHub Releases](https://github.com/atomicassets/atomicmarket-contract/releases).

Entry headings keep the `## [X.Y.Z] - YYYY-MM-DD` form. Each entry opens with a
summary line, then carries the sections that `RELEASING.md` defines, in that
order; the entry is the editorial text of the version's GitHub Release. This
project follows semantic versioning.

## [2.0.0] - 2026-08-03

The AtomicMarket v2 contract: royalty splits, single-asset listings, and a collection fee charged at execution time.

### Breaking changes

- A sale, auction or buyoffer references exactly one asset, and `announcesale`, `announceauct` and `createbuyo` reject more than one. Listing several assets in one transaction covers what bundles were used for, and single-asset rows keep per-asset attribution of the collection fee and the royalty split exact. (#1)
- A pre-v2 row holding more than one asset cancels when it is touched, rather than migrating. `purchasesale` cancels the sale and charges the buyer nothing, a bid or claim on a bundle auction dissolves it with the bid refunded and the assets returned, and `acceptbuyo` refunds the buyer. `cancelsale` and `cancelauct` on a bundle may be called by anyone, and activating a bundle through the `sale` offer memo or the `auction` transfer memo aborts. (#1)
- A partially claimed bundle auction finishes through the normal claim actions, because one side was already served. The collection fee goes to the author in full and no royalty logs are emitted. (#2)
- Settlement reads the collection's `market_fee` live from AtomicAssets at execution time, not the value stored when the listing was created. A fee change by the author, up or down within the 15% cap, applies at once to every existing listing, and the listing row's `collection_fee` is informational only. The buyer always pays the listed price and only the split between seller and collection moves, so a front end shows the live fee at the point of sale. (#3)

### Upgrading

| Asset | sha256 |
| --- | --- |
| `atomicmarket.wasm` | `5016d9560574cce18f511de41d4ba1e81c4d30526c4452f0f70c3e59d64e7ed3` |
| `atomicmarket.abi` | `e1b480faeb6f59f8f474af28677907a25d0d58e455d5a951f2315a87012177a9` |

- The wasm sha256 equals the on-chain code hash, so `get_code_hash` confirms which bytes are running. The attached `SHA256SUMS` carries the same two values.
- The ABI is additive against v1.3.3: nothing is removed and no existing struct changes shape. The published ABI is the legacy-compat build, where `vector<uint8_t>` fields render as `uint8[]`, so a v1 reader keeps working unchanged. The ABI version moves from `eosio::abi/1.1` to `1.2`, and the behavior changes above are not expressed in the ABI at all.
- The final surface is 51 actions and 12 tables. The existing tables (`sales`, `auctions`, `buyoffers`, `tbuyoffers`, `config`, `balances`, `marketplaces`, `bonusfees`, counters) keep their layout, and the added tables are `royaltyconf`, `royaltytemp` and `royaltyattr`.
- Deploy AtomicAssets v2 on the chain first. Settlement reads the AtomicAssets `templates2` table for royalty attribute matching, and the collections row for the live market fee.
- A chain that does not yet run template buyoffers also gains `createtbuyo`, `canceltbuyo`, `fulfilltbuyo`, `lognewtbuyo` and the `tbuyoffers` table with this upgrade.
- The deploy is a `setcode` plus `setabi`. On-chain state is preserved, and existing single-asset sales, auctions and buyoffers keep working.
- `setversion` takes `2.0.0`.
- These bytes ran on the WAX testnet and jungle4 as rc2, where the on-chain code hash matched the table above. That was the first AtomicMarket v2 deployment on any chain.
- Signers of an msig proposal check the proposal's wasm sha256 against the table above, and its packed ABI against the published `.abi`, because the chain does not validate `setabi` payloads.

### Features

- By default the collection fee goes to the collection author, and an author can instead split it across weighted categories: a global founders list, per-template recipient lists, and attribute rules that match a `(field, value)` on the asset such as `rarity = legendary`. Configuration lives in the new `royaltyconf`, `royaltytemp` and `royaltyattr` tables. (#1)
- Settlement emits `logroyfound`, `logroytempl`, `logroyattr` and `logroydust`, whose amounts sum exactly to the collection fee, so an indexer records the final per-recipient amounts without reimplementing the split. These actions notify no account, so read them from action traces, and a `logroyattr` rule id is never reused. Payouts accrue to balances and are claimed with `withdraw`, and nothing is transferred inline, so a recipient contract cannot block a collection's settlements. (#1)
- `setdefmktcr` redirects the empty-name default marketplace's fee recipient at runtime, and `migratebal` merges accumulated balances, so one binary runs on a chain where the seeded `fees.atomic` account does not exist. `migratebal` rejects `from == to`, which would otherwise double a balance. (#7)
- Tables are constructed lazily per action, the config singleton is deserialized at most once per action, the AtomicAssets collections row is read through a size-capped partial read that takes only the author and the market fee, and notification handlers bind with `[[eosio::on_notify]]`. (#1)

### Bug fixes

- `assertsale`, `assertauct` and `acceptbuyo` compare asset lists with the four-iterator `std::is_permutation`, which checks the lengths and cannot read past the shorter list. `6da1ed7`
- Settlement re-asserts the execution-time fee between 0 and 15 percent, because a negative double cast to `uint64_t` is undefined behavior, and `acceptbuyo` and `fulfilltbuyo` check the AtomicAssets offers table before reading its last row. `2b0338f`

### Other changes

- `setmarketfee` and `addbonusfee` reject a fee configuration whose fees plus the maximum collection fee exceed the sale price, and `internal_payout_sale` asserts a positive seller payout as the runtime backstop. (#4)
- Custodial rentals are not part of v2. The implementation is preserved on the [`archive/v2-custodial-rentals`](https://github.com/atomicassets/atomicmarket-contract/tree/archive/v2-custodial-rentals) branch. (#13)
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@ AtomicMarket is a marketplace to sell and auction [AtomicAssets](https://github.
- **Collection fees**

Collections can define a market fee between 0 and 15% in the AtomicAssets contract. This fee is respected by the AtomicMarket and paid to the authors of the collection.

## Releases

Tagged versions are published as GitHub Releases with the `atomicmarket.wasm`,
`atomicmarket.abi` and `SHA256SUMS` assets attached.
[RELEASING.md](RELEASING.md) covers how a release is cut and what its notes
carry, and [CHANGELOG.md](CHANGELOG.md) holds the notes for each version.

## Credits

AtomicMarket is the work of many contributors, recorded in
[AUTHORS.md](AUTHORS.md).
232 changes: 232 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Releasing atomicmarket-contract

How a version of this contract reaches GitHub Releases. A release ends at a
rendered Release carrying `atomicmarket.wasm`, `atomicmarket.abi` and
`SHA256SUMS` as assets, not at the pushed tag: those checksums are what a
deployer pins and what a signer of a multi-party proposal verifies against.

Tags are `vX.Y.Z` (`v2.0.0`), and a release candidate is `vX.Y.Z-rcN`
(`v2.0.0-rc2`). The release artifacts are built with the CDT version CI pins,
4.1.1, so a reader can rebuild the tag and get the hashes the notes name.

## Checklist

1. The feature PR carries the `CHANGELOG.md` entry for the version under
`## [X.Y.Z]`, written in the section shape below with H3 headings. Its
`### Upgrading` states the ABI compatibility with the previous stable release
(byte-identical, additive, or breaking with the migration a consumer makes)
and the `setversion` value, which is the core semver of the tag. The rows of
the checksum table are left empty here, because the build in step 2 produces
them. The entry is the editorial text of the Release, so it is written once,
in the PR that makes the change.

2. Build the release artifacts from a clean tree and write the checksum file:

```sh
make clean
make release
(cd build && sha256sum atomicmarket.wasm atomicmarket.abi) > SHA256SUMS
```

`make release` compiles with the pinned CDT and patches the ABI back to the
legacy spellings, which is what the released `.abi` carries. It needs the
CDT installed natively. Without a native install, `bash build.sh` compiles
with the same pinned CDT in the `antelope-cdt` docker image, and
`python3 scripts/patch-abi.py build/atomicmarket.abi` applies the same
patch:

```sh
make clean
bash build.sh
python3 scripts/patch-abi.py build/atomicmarket.abi
(cd build && sha256sum atomicmarket.wasm atomicmarket.abi) > SHA256SUMS
```

The checksum file uses bare asset names, so it verifies against the
downloaded assets in step 6. Copy its two rows into the entry's
`### Upgrading` table:

```
| Asset | sha256 |
| --- | --- |
| `atomicmarket.wasm` | `<sha256>` |
| `atomicmarket.abi` | `<sha256>` |
```

Land that as a `chore(release): X.Y.Z` commit touching `CHANGELOG.md`
alone. The wasm and the ABI do not depend on `CHANGELOG.md`, so a build of
the tag reproduces the hashes the entry names. `SHA256SUMS` is a release
asset rather than a committed file, and it stays out of the commit. A
stable release that ships the last candidate's build has identical rows
already in the entry: tag the candidate's commit and land nothing new.

3. Preview the body before anything is tagged:

```sh
scripts/release-notes.sh vX.Y.Z main
```

The preview composes the body from the `CHANGELOG.md` entry at that branch
and the commits since the previous tag, and it fails when the entry is
missing. It does not check the section names, so read the preview against
the template below. Pass `origin/main` in a clone without a local `main`.
The compare link is built from the `origin` remote, so cut the release from
a clone whose `origin` is this repository, not a fork.

4. Tag the release commit and push the tag:

```sh
git tag vX.Y.Z && git push origin vX.Y.Z
```

A candidate is tagged `vX.Y.Z-rcN`. Push the tag before creating the
Release, because `--verify-tag` refuses a tag the remote does not have.

5. Compose the body, read it, then create the Release with its assets:

```sh
scripts/release-notes.sh vX.Y.Z > notes.md
gh release create vX.Y.Z --verify-tag --title vX.Y.Z --notes-file notes.md \
build/atomicmarket.wasm build/atomicmarket.abi SHA256SUMS
```

Add `--prerelease` for a `-rcN` tag, so the candidate does not become the
repository's latest Release. Add `--latest=false` when the Release is for a
tag older than the current latest one, so the latest marker does not move
backwards. With more than one release in flight, create them in ascending
version order.

6. Verify the published Release against its own assets:

```sh
gh release download vX.Y.Z --dir /tmp/vX.Y.Z
(cd /tmp/vX.Y.Z && sha256sum -c SHA256SUMS)
```

The body's table names the same two hashes. When either check fails, never
re-attach an asset and never move the tag on a published Release: a
consumer that pinned these hashes fails closed on any change, and a signer
may already have verified a proposal against them. Cut the next patch
version instead.

Deploying the contract, whether by key or by multi-party proposal, and the
`setcode`, `setabi`, `setversion` and resource steps that go with it, stays with
the deployer's own procedure. The Release is what they pin and verify against.

## Body template

The Release title is the tag name verbatim. The body is an optional
one-sentence summary, then the sections that have items, then the commit list,
then the compare link as the last line. Nothing follows the link, and a section
with no items is left out.

```
<one-sentence summary, optional>

## Breaking changes

- <what changed, and what the reader does about it>. (#N)

## Upgrading

- <what the move from the previous stable release takes: the checksums, the ABI compatibility, the setversion value>.

## Features

- <what is new>. (#N)

## Bug fixes

- <what was wrong and is not now>. (#N)

## Security

- <the advisory or the dependency lift, named>. (#N)

## Deprecations

- <what is deprecated and what replaces it>. (#N)

## Other changes

- <a change a consumer notices that fits no section above>. (#N)

## Commits

- <short sha> <subject>

Full changelog: https://github.com/atomicassets/atomicmarket-contract/compare/<PREV>...<TAG>
```

The section order is breaking changes, upgrading, features, bug fixes,
security, deprecations, other changes.

A Release body carries no credits section. Contributor and lineage credit lives
in [AUTHORS.md](./AUTHORS.md) and the README, where it is maintained once
rather than restated per version.

`## Upgrading` is for the deployer and the integrator, and it is written against
the previous stable release rather than against the tag range the commit list
covers. For this contract it carries the checksum table of the released wasm and
ABI, the statement of how the ABI compares with the previous stable release
(byte-identical, additive, or breaking with the migration a consumer makes), the
`setversion` value, and any ordering or resource note the deploy depends on. A
candidate body may confine it to the change since the previous candidate that
has a Release, because that is the move a test deployment makes; the stable body
describes the whole move. One table is allowed here, and it lists the released
artifact checksums. Items elsewhere stay bullets.

`## Security` carries advisories and dependency lifts, each naming its GHSA or
CVE identifier. A release with neither section leaves both out.

## Voice

- Neutral and factual, the register of the Node.js or esbuild release notes.
- Sectioned. The heading says what kind of change it is, so the item does not
repeat it.
- One to three plain sentences per item: what changed, and what the reader does
about it when action is needed. Code identifiers in backticks.
- Every item ends with its PR reference `(#N)`, or with its short sha in
backticks when the change had no PR. An `## Upgrading` item that states a
deploy fact rather than a change, such as a checksum or an ABI that has not
moved, carries no reference.
- No preface, no motivation essay, no clause chain explaining how the author got
there. The why stays only where it changes what the reader does.
- Present tense for the new behavior, sentence-case headings, straight quotes,
and no em-dash.

## The CHANGELOG entry

`CHANGELOG.md` is where the editorial text is written, and the Release body is
that entry with its headings promoted one level.

An entry heading is `## [X.Y.Z]`, optionally followed by ` - YYYY-MM-DD`. Under
it comes an optional one-line summary, then the H3 sections in the order above
(`### Breaking changes`, `### Upgrading`, and the rest). A candidate tag
`vX.Y.Z-rcN` reads the `## [X.Y.Z]` entry as it stands at that tag, so a
candidate body shows the notes for the version so far and the stable body shows
the finished entry.

## Tag ranges, candidates, and older releases

- `PREV` for a stable tag is the nearest earlier stable `v*` tag, so a stable
release lists every commit since the last stable release and skips the
candidates between them. `PREV` for a candidate tag is the nearest earlier tag
of any kind, which is usually the previous candidate. A stable tag whose only
earlier tags are candidates takes the nearest of them, so the first stable
release after a candidate line lists what it adds to the last candidate.
- Tags from the upstream v1 line count as earlier tags, so `v2.0.0` lists the
commits since `v1.3.3`. Those v1 tags carry no Release of their own.
- `## Commits` lists the whole `PREV..TAG` range, oldest first, including the
release commit. Its line count equals `git rev-list --count PREV..TAG`.
- A tag with no earlier tag has no `PREV`. Its body is the summary and the
sentence `Initial release.`, with no commit list and no compare link, and it
is written by hand.
- A candidate tag is created with `--prerelease`, and a Release created for a
tag older than the current latest is created with `--latest=false`.

`scripts/release-notes.sh` needs bash, git, awk and sed. Without a ref it reads
`CHANGELOG.md` at the tag rather than from the working tree, so the body
describes what the tag ships. It exits non-zero and names what is missing when
no tag is given, when the tag does not exist, when the CHANGELOG at that ref
carries no entry for the version, and when no earlier tag exists.
Loading
Loading