Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
ca6fd4c
feat(rentals): non-custodial rental flow over AtomicAssets lease prim…
robrigo Jun 29, 2026
e05dba8
refactor(rentals): address review (simplify lease-state checks, refre…
robrigo Jun 29, 2026
3e032f8
test(rentals): refresh AA fixture (leaseextend expiry guard)
robrigo Jun 29, 2026
cb16f50
docs(rentals): drop stale pretitle references, note cancelrent precon…
robrigo Jun 29, 2026
abe2b34
fix(rentals): cap extensions by total duration from lease start, not …
robrigo Jun 29, 2026
78f42f1
refactor(rentals): collapse holder + is_rented into a single renter f…
robrigo Jun 29, 2026
9074544
refactor(rentals): drop leases.market mirror and the market arg on in…
robrigo Jun 29, 2026
e8613b7
docs(rentals): note the lock-refusal helper also guards announcerent …
robrigo Jun 29, 2026
a099035
test: refresh bundled AtomicAssets fixture to the reclaim-hardened build
robrigo Jun 29, 2026
32bd125
refactor(rentals): drop the lease-state mirror, AA leases is the sour…
robrigo Jun 30, 2026
658ef1e
fix(rentals): make endrent idempotent; correct the flow doc (Copilot …
robrigo Jun 30, 2026
a07f751
feat(rentals): editrent + delist-during-lease, richer logrental, rent…
robrigo Jul 2, 2026
ec183bc
test(rentals): cover idempotent endrent, invalid-listing cancel, delp…
robrigo Jul 2, 2026
1e13bfd
docs(rentals): rewrite the wiki page for the non-custodial model
robrigo Jul 2, 2026
457584d
docs(rentals): document editrent, delist-vs-lease, rental_id linkage,…
robrigo Jul 2, 2026
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
138 changes: 81 additions & 57 deletions docs/wiki/Rentals.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Rentals

AtomicMarket V2 supports **custodial per-hour rentals** built on the AtomicAssets V2
holdership system: while an asset is rented, the renter is its **holder** (visible to games
and dApps through the AtomicAssets `holders` table) while the **ownership** stays with the
market contract, so the renter can never run away with the asset.
AtomicMarket supports **non-custodial per-hour rentals**. While an asset is rented, the renter
becomes the **real AtomicAssets owner** of it, so unmodified games, APIs and dApps grant the renter
the asset's utility with no special integration. The lister's right to get the asset back is parked
in the AtomicAssets `leases` table, the **single source of truth for the lock state**. The asset is
locked from transfer, burn and offer-out for the duration, and a permissionless `reclaim` force-returns
it to the lister at expiry. The asset is **never escrowed** in the market contract.

## Lifecycle

```
announcerent ──> transfer (memo "rental") ──> rentasset ──> [extensions] ──> endrent ──┐
│ (custody) (rented) (listed again)│
└──────────────────────────── cancelrent (asset returned) <────────────────────────┘
announcerent ──> rentasset ──> [extensions] ──> expiry ──> endrent / reclaim ──┐
│ (renter = owner, locked) (returned, listed again)│
├── editrent (reprice / re-bound, any time) │
└── cancelrent (delist, any time; reclaim-if-expired) <────────────────────────┘
```

1. **List** — the owner announces the listing with its terms:
1. **List** — the owner announces the listing with its terms. The asset stays in the owner's account;
announcing does not move or lock it.

```sh
cleos push action atomicmarket announcerent '{
Expand All @@ -26,18 +30,15 @@ announcerent ──> transfer (memo "rental") ──> rentasset ──> [extensi
}' -p alice@active
```

- `price_per_hour` is denoted in the listing symbol. If it differs from
`settlement_symbol`, a delphi pair must be configured and the rental is paid in the
settlement symbol at the oracle rate at renting time (e.g. price in USD, paid in WAX).
- `maximum_rental_duration` (seconds) is the longest period one rental — including
extensions by the same renter — can cover. Minimum 3600 (one hour), maximum 28 days
(2,419,200 seconds).
- `price_per_hour` is denoted in the listing symbol. If it differs from `settlement_symbol`, a
delphi pair must be configured and the rental is paid in the settlement symbol at the oracle
rate at renting time (e.g. price in USD, paid in WAX).
- `maximum_rental_duration` (seconds) is the longest period one rental — including extensions by
the same renter, measured from the lease's original start — can cover. Minimum 3600 (one hour),
maximum 28 days (2,419,200 seconds). AtomicAssets enforces its own 28-day cap independently.
- Each asset is its own listing (one listing per asset id).

2. **Activate** — the owner transfers the asset to the market account with the memo
`rental`. The contract becomes the custodial owner. Each asset is its own listing
(one listing per asset id).

3. **Rent** — a renter pays from their deposited balance:
2. **Rent** — a renter pays from their deposited balance:

```sh
cleos push action atomicmarket rentasset '{
Expand All @@ -50,62 +51,85 @@ announcerent ──> transfer (memo "rental") ──> rentasset ──> [extensi
}' -p bob@active
```

- The total price (`price_per_hour × hours`, oracle-converted if applicable) is deducted
from the renter's balance and paid out like a sale: marketplace fees, the collection
fee (with [royalty splits](Royalty-Splits) and
[collection fee](V2-Changes)), remainder to the listing owner.
- The AtomicAssets holdership of the asset moves to the renter until `rental_end`.
- `expected_price_per_hour` protects the renter against listing changes between signing
and execution.

4. **Extend** — the *current* renter can call `rentasset` again while their rental is
active; the purchased hours are appended to the current period. The combined remaining
period must stay within `maximum_rental_duration`. Rentals never renew automatically.

5. **Wrap up** — after `rental_end`, **anyone** may call `endrent(asset_id)`. It moves the
holdership back to the market contract, making the listing rentable again. If nobody
calls it, the next `rentasset` moves holdership directly from the expired renter to the
new renter.

6. **Cancel** — the owner reclaims the asset with `cancelrent(asset_id)` whenever no rental
is actively running (also handles expired-but-not-ended rentals by reclaiming holdership
first). A not-yet-activated listing whose owner no longer owns the asset is invalid and
may be cancelled by anyone.
- The total price (`price_per_hour × hours`, oracle-converted if applicable) is deducted from the
renter's balance and paid out like a sale: marketplace fees, the collection fee (with
[royalty splits](Royalty-Splits) and [collection fee](V2-Changes)), remainder to the listing owner.
- AtomicMarket drives AtomicAssets to make the renter the **real owner** of the asset until
`rental_end`, and the asset is locked (its lock state lives in the AtomicAssets `leases` row).
- `expected_price_per_hour` (and `intended_delphi_median` for oracle-priced listings) protect the
renter against listing or price changes between signing and execution.

3. **Extend** — the *current* renter can call `rentasset` again while their rental is active; the
purchased hours are appended to the current period. The combined period, measured from the lease's
original start, must stay within `maximum_rental_duration`. An extension is a fresh purchase of
the listing's **current** terms (see `editrent`). Rentals never renew automatically.

4. **Edit** — the owner can change the price, maximum duration and maker marketplace with
`editrent` at **any** time, including while a lease runs: the listing row is the owner's offer
of *future* rentals, separate from the renter's already-purchased lease (which is untouched).
The listing and settlement symbols cannot change — cancel and relist for that. Renters are
protected against repricing races by `expected_price_per_hour`.

5. **Wrap up** — after `rental_end`, **anyone** may call `endrent(asset_id)`, which triggers the
permissionless AtomicAssets `reclaim`: it returns the asset to the lister and clears the lock,
making the listing rentable again. `endrent` is idempotent — a second call once the asset is
already reclaimed is a no-op — and purely lease-driven, so it works even for delisted leases.
If nobody calls it, the next `rentasset` reclaims the expired lease and re-leases to the new
renter in the same transaction. Until someone reclaims, the renter keeps the asset's utility
past `rental_end` for free — listers (or the platform's keeper cron) should call `endrent`
promptly.

6. **Cancel** — the owner removes the listing with `cancelrent(asset_id)` at any time. Delisting is
**not** termination: during an active rental it only withdraws the offer of future rentals; the
lease runs to its end and is then reclaimed as usual. Because nothing is escrowed, cancelling is
just removing the listing row; an expired-but-unreclaimed rental is reclaimed to the owner as
part of the cancel. A listing whose owner no longer owns the asset (while unleased) is invalid
and may be cancelled by anyone.

## Actions

| Action | Auth | Effect |
|---|---|---|
| `announcerent(lister, asset_id, price_per_hour, settlement_symbol, maximum_rental_duration, maker_marketplace)` | lister | Create a rental listing |
| `cancelrent(asset_id)` | owner (anyone if invalid) | Cancel the listing; return the asset if custodied |
| `announcerent(lister, asset_id, price_per_hour, settlement_symbol, maximum_rental_duration, maker_marketplace)` | lister | Create a rental listing (no escrow) |
| `editrent(asset_id, new_price_per_hour, new_maximum_rental_duration, new_maker_marketplace)` | owner | Change the offered terms, even mid-lease; symbols immutable |
| `cancelrent(asset_id)` | owner (anyone if invalid) | Delist (lease unaffected); reclaim first if expired-but-unreclaimed |
| `rentasset(renter, asset_id, rental_hours, expected_price_per_hour, intended_delphi_median, taker_marketplace)` | renter | Rent or extend; pays from the renter's balance |
| `endrent(asset_id)` | anyone | Reset an expired rental back to its listed state |
| `endrent(asset_id)` | anyone | After expiry, reclaim the asset to the lister (idempotent, lease-driven) |
| `payrentram(payer, asset_id)` | payer | Take over the RAM cost of the listing row |

Log actions: `lognewrent` (listing created), `logrentstart` (custody received, listing
active), `logrental` (rental executed: renter, hours, paid price, rental_end).
Market log actions: `lognewrent` (listing created), `logeditrent` (terms changed), `logrental`
(rental executed: `rental_id`, renter, hours, paid price, `rental_start`, `rental_end`,
`is_extension`). The lock and return events are logged on the AtomicAssets side: `loglock` (lease
opened or extended — distinguishable via `rental_start`) and `logreclaim` (asset returned); both
echo the `rental_id` of the rental that opened the lease, so indexers can join them to the market's
rental without parsing memos. The reclaim path intentionally notifies **no account that could veto
it** (not the renter, not the title owner) so the guaranteed return cannot be aborted — only the
asset's collection is notified. (Corollary: a collection notify contract that throws on `loglock`
can selectively veto lease *creation* for its assets — a de facto per-collection opt-out.)

## The `rentals` table

The row is the owner's offered terms (mutable via `editrent`); the lock/renter/end state lives in
the AtomicAssets `leases` table, and the two join 1:1 on `asset_id`.

| Field | Meaning |
|---|---|
| `asset_id` | primary key — one listing per asset |
| `owner` | the listing creator; receives the rental payouts |
| `holder` | the current renter; empty when not rented out |
| `price_per_hour` | in the listing symbol |
| `settlement_symbol` | what rentals are actually paid in |
| `maximum_rental_duration` | seconds; cap for a single rental incl. extensions |
| `rental_end` | seconds since epoch; 0 when not rented out (secondary index `rentalends`) |
| `asset_transferred` | true once the asset is in contract custody |
| `maker_marketplace`, `collection_name`, `collection_fee` | listing metadata |
| `maximum_rental_duration` | seconds; cap for a single rental incl. extensions, from the lease start |
| `maker_marketplace` | the marketplace that brokered the listing |
| `collection_name` | the asset's collection |
| `collection_fee` | the collection fee snapshot at listing time (the live fee is re-read at payout) |

## Integration notes for games and dApps

- To honor rentals, resolve an asset's *effective user* through the AtomicAssets `holders`
table: if a row exists for the asset, the `holder` is the active user; otherwise the
owner is. Rented assets have `owner = atomicmarket` and `holder = <renter>`.
- `rental_end` is tracked in the market's `rentals` table (with a secondary index by end
time for expiry sweeps). After expiry the holdership remains with the previous renter
until `endrent`, the next rental, or `cancelrent` resets it — treat
`rental_end <= now` as "rental over" regardless of the holders table.
- To honor rentals, treat the AtomicAssets **owner as the effective user**: during a lease the renter
*is* the owner, so no special resolution is needed for utility. The lock state (whether a returnable
lease is in force, and when it ends) is the AtomicAssets `leases` row for the asset.
- The `leases` row carries `title_owner` (the lister the asset returns to), `renter`, `rental_start`,
`rental_end` and the opening `rental_id`. Treat `rental_end <= now` as "rental over"; after expiry
the renter remains the owner until `endrent`, the next `rentasset`, or `cancelrent` triggers the
reclaim.
- Royalties apply to rentals exactly as to sales, including the royalty log actions.
27 changes: 18 additions & 9 deletions include/atomicassets-interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,26 @@ namespace atomicassets {
typedef multi_index <name("assets"), assets_s> assets_t;


struct holders_s {
// Non-custodial rental lock/title record. A row's existence means the asset
// is actively leased and locked: the renter is the real owner and title_owner
// holds the reclaim right until rental_end.
Comment on lines +151 to +153
struct leases_s {
uint64_t asset_id;
name holder;
name owner;
name title_owner;
name renter;
name collection_name;
uint32_t rental_start;
uint32_t rental_end;
uint64_t rental_id;

uint64_t primary_key() const { return asset_id; };
uint64_t by_holder() const { return holder.value; };
uint64_t primary_key() const { return asset_id; };
uint64_t by_title_owner() const { return title_owner.value; };
uint64_t by_rental_end() const { return (uint64_t) rental_end; };
};
typedef multi_index <name("holders"), holders_s,
indexed_by<name("holder"), const_mem_fun <holders_s, uint64_t, &holders_s::by_holder>>>
holders_t;
typedef multi_index <name("leases"), leases_s,
indexed_by<name("titleowner"), const_mem_fun <leases_s, uint64_t, &leases_s::by_title_owner>>,
indexed_by<name("rentalend"), const_mem_fun <leases_s, uint64_t, &leases_s::by_rental_end>>>
leases_t;


struct offers_s {
Expand Down Expand Up @@ -228,6 +237,6 @@ namespace atomicassets {
template_mutables_t get_template_mutables(name collection_name) {return template_mutables_t(ATOMICASSETS_ACCOUNT, collection_name.value);}

assets_t get_assets(name owner) {return assets_t(ATOMICASSETS_ACCOUNT, owner.value);}
holders_t get_holders() {return holders_t(ATOMICASSETS_ACCOUNT, ATOMICASSETS_ACCOUNT.value);}
leases_t get_leases() {return leases_t(ATOMICASSETS_ACCOUNT, ATOMICASSETS_ACCOUNT.value);}

};
Loading
Loading