Skip to content

Comments

Non-fungible token unshielded#19

Closed
emnul wants to merge 283 commits intomainfrom
erc721-unshielded
Closed

Non-fungible token unshielded#19
emnul wants to merge 283 commits intomainfrom
erc721-unshielded

Conversation

@emnul
Copy link
Contributor

@emnul emnul commented Apr 7, 2025

Brings an implementation of an ERC721-like specification to Compact

This will not compile unless you use compactc v0.23.0

Closes #123 #6 #129

Copy link
Member

@0xisk 0xisk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks you @emnul! In general looks solid. However, I didn't fully yet finish reviewing all the circuits but, those are comments and questions for now.

Also at the mean time, I would ask please if you can add documentation for the circuits that doesn't have yet.

@emnul
Copy link
Contributor Author

emnul commented Apr 25, 2025

Also at the mean time, I would ask please if you can add documentation for the circuits that doesn't have yet.
@0xisk

For sure I was waiting until the code was in a more stable state before I started writing docs. I will be adding the docs once the tests have been completed / finalized. There are a couple assumptions I made with the use of Field values for the tokenId I want to make sure hold.

Copy link
Contributor

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start on this, @emnul! I know we're still waiting on the bug fix in the compiler and tests, but I left some initial comments

@andrew-fleming andrew-fleming mentioned this pull request May 19, 2025
@emnul emnul force-pushed the erc721-unshielded branch from 54b3a2c to 6de401d Compare May 29, 2025 03:23
@emnul emnul added the enhancement New feature or request label Jun 3, 2025
@emnul emnul requested a review from 0xisk June 3, 2025 03:43
@emnul emnul marked this pull request as ready for review June 3, 2025 03:44
Copy link
Member

@0xisk 0xisk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @emnul great work! I left comments but the main discussion is related to the unsafe circuits that is mentioned in this thread #19 (comment).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safe & Unsafe Transfer Strategy for ERC Token Migration

I know our team agreed not to accept ContractAddress in transfer right now—if someone sends tokens to a contract, they can’t pull them back because we lack C2C calls. My worry is that if we bake both ZswapCoinPublicKey and ContractAddress into the types today, migrating later when C2C is supported will be painful: it’ll touch the ledger definitions and break every contract that uses this token.

Instead, I’d propose we keep the “final” types from day one (i.e. Either<ZswapCoinPublicKey, ContractAddress>), but restrict the main circuits so they only succeed when the recipient is a ZswapCoinPublicKey. Then we provide a separate, clearly‐named “unsafe” variant for anyone who really needs to send to a contract (knowing that, right now, the contract can’t send funds back). That gives us two options:

  1. Safe‐only circuit

    • Signature: transfer(to: Either<ZswapCoinPublicKey, ContractAddress>, amount: Uint128)
    • Behavior: if to is a contract, revert immediately; if to is a public key, do the transfer.
  2. Safe + “unsafe” circuit

    • Safe version as in option 1 above (rejects contracts).
    • Add _unsafe_transfer(to: Either<ZswapCoinPublicKey, ContractAddress>, amount: Uint128) that always allows both types—contracts just assume they can’t send back until we enable C2C.

Option 2 is my preference. Once C2C support is live, we can remove the isContract check in transfer — allowing contracts to work normally — and deprecate _unsafe_transfer without touching ABIs or storage layouts. Contracts that already call _unsafe_transfer keep working, and everyone else just uses transfer.

When we are ready for a major release, we can remove _unsafe_transfer entirely:

  1. Deprecation phase (minor version):

    • Mark _unsafe_transfer as deprecated in the docs and emit a warning if possible.
    • Keep its implementation intact so existing callers continue to work.
  2. Major release:

    • Drop _unsafe_transfer and ensure transfer has no isContract guard.
    • By this point, anyone using _unsafe_transfer should have migrated to the now C2C-capable transfer.

Example (Option 2 sketch):

// Public “safe” transfer: only succeeds if `to` is a user key
export circuit transfer(
  to: Either<ZswapCoinPublicKey, ContractAddress>,
  amount: Uint<128>
): [] {
 assert (!isContractAddress(to)) "Use _unsafe_transfer to send to contracts";
  // ...normal user‐to‐user move...
}

// Explicit “unsafe” transfer: allows both keys and contracts
export circuit _unsafe_transfer(
  to: Either<ZswapCoinPublicKey, ContractAddress>,
  amount: Uint<128>
): [] {
  if (to.isZswapCoinPublicKey()) {
    // ...user‐to‐user move...
  } else {
    // ...user‐to‐contract move (caller assumes contract cannot send back)...
  }
}

With this pattern, when C2C calls exist, we simply remove the isContractAddress() guard from transfer. At that point, contracts can receive and send tokens normally, and _unsafe_transfer becomes redundant (so we can deprecate or remove it in the next major release).

Copy link
Member

@0xisk 0xisk Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ERC-721 Circuits Requiring Unsafe Variants vs. Harmless Circuits

Circuits that need an unsafe variant (because they move tokens to a recipient, which could be a ContractAddress):

  • transferFrom(from: Either<…>, to: Either<…>, tokenId: Uint<128>)

    • This is the main public transfer. As soon as you allow to: Either<…>, you need _unsafe_transferFrom to bypass the “reject contract” check.
  • _transfer(from: Either<…>, to: Either<…>, tokenId: Uint<128>)

    • Internal direct‐transfer helper. If you switch its to type to Either<…>, add _unsafe_transfer so it can send into contracts.
  • _mint(to: Either<…>, tokenId: Uint<128>)

    • Minting is effectively “transfer from zero.” If you allow to = Either<…>, you must have _unsafe_mint to let a contract receive an NFT (knowing it can’t send back until C2C exists).

Circuits that remain harmless (no unsafe variant needed):

  • initialize(name_: Opaque<"string">, symbol_: Opaque<"string">)

  • name(): Opaque<"string">

  • symbol(): Opaque<"string">

  • balanceOf(owner: Either<…>): Uint<128>

  • ownerOf(tokenId: Uint<128>): Either<…>

  • tokenURI(tokenId: Uint<128>): Opaque<"string">

  • approve(to: Either<…>, tokenId: Uint<128>)

    • Approvals don’t actually send tokens, so approving a contract is “harmless” today.
  • getApproved(tokenId: Uint<128>): Either<…>

  • setApprovalForAll(operator: Either<…>, approved: Boolean)

  • isApprovedForAll(owner: Either<…>, operator: Either<…>): Boolean

  • _requireOwned(tokenId: Uint<128>): Either<…>

  • _ownerOf(tokenId: Uint<128>): Either<…>

    • _update(to: Either<…>, tokenId, auth) can be written once to accept both address types, without any “unsafe” variant.
  • _approve(to: Either<…>, tokenId: Uint<128>, auth: Either<…>)

  • _checkAuthorized(owner: Either<…>, spender: Either<…>, tokenId: Uint<128>)

  • _isAuthorized(owner: Either<…>, spender: Either<…>, tokenId: Uint<128>): Boolean

  • _getApproved(tokenId: Uint<128>): Either<…>

  • _setApprovalForAll(owner: Either<…>, operator: Either<…>, approved: Boolean)

  • _burn(tokenId: Uint<128>)

    • Burning can only remove (send to zero), so there’s no “send into a contract” risk here.
  • _setTokenURI(tokenId: Uint<128>, tokenURI: Opaque<"string">)

In short, only the circuits that actually move tokens “to” an address require an _unsafe_… counterpart; all read‐only or pure‐approval methods are safe as-is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @0xisk! We should provide this migration plan more formally in our docs

@emnul emnul force-pushed the erc721-unshielded branch from c267e00 to 8c24d07 Compare June 4, 2025 18:14
@emnul emnul requested a review from 0xisk June 4, 2025 19:23
Copy link
Member

@0xisk 0xisk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @emnul! the changes look great, just left some comments mostly are minor. Please check this PR I added more tests which I think they are not covered. #92

@emnul emnul requested a review from a team as a code owner June 5, 2025 13:40
@emnul emnul force-pushed the erc721-unshielded branch 2 times, most recently from 8ab6b72 to 8605323 Compare June 5, 2025 14:20
@emnul
Copy link
Contributor Author

emnul commented Jun 5, 2025

@0xisk I realize now I might have used the Initializable module incorrectly. I'm not sure if it's intended to be imported in the top level module or in the sub-modules themselves. I'm using #38 as a reference. What do you think? Also the current structure introduces some challenges with testing.

Copy link
Member

@0xisk 0xisk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @emnul LGTM! Well done!

@0xisk
Copy link
Member

0xisk commented Jun 6, 2025

@0xisk I realize now I might have used the Initializable module incorrectly. I'm not sure if it's intended to be imported in the top level module or in the sub-modules themselves. I'm using #38 as a reference. What do you think? Also the current structure introduces some challenges with testing.

In the #38 PR, I think it is not completed yet. The idea of initializable module is make sure that all the circuits in your module are being called after the initialize() circuit, as in this PR: #95

@emnul emnul requested a review from 0xisk June 6, 2025 17:39
Copy link
Contributor

@andrew-fleming andrew-fleming left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good improvements @emnul! Sorry for the billions of comments 😅 most of them are minor readability suggestions and improvements in doc/test consistency. The big things though are:

  • Change the token name
  • Simplify circuit logic. Basically treat the safe variants as wrappers that only check that the recipient is not a contract address and then invoke the unsafe variant with the sanitized recipient

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @0xisk! We should provide this migration plan more formally in our docs

@emnul emnul changed the title ERC721 unshielded Non-fungible token unshielded Jun 12, 2025
@emnul emnul self-assigned this Jun 15, 2025
@emnul emnul requested a review from andrew-fleming June 15, 2025 02:42
@emnul emnul added the under-review Pull requests being reviewed by OpenZeppelin core team. label Jun 15, 2025
andrew-fleming and others added 24 commits June 26, 2025 03:30
* test: adding more tests in ERC20 contract

* catch mint overflow to output a more readable error msg

* add overflow tests

* fix test assertions

* add initial module doc

* remove contract dir

* re-add erc20

* use utils zero address check in erc20

* fix sim export

* remove file

* remove unused type

* add line

* set metadata as sealed

* add initializer comment

* add return comment to initializer

---------

Co-authored-by: 0xisk <iskander.andrews@openzeppelin.com>
* add pausable

* update repo structure

* move mock contracts to mocks dir

* fix initializable tests

* move mocks to test

* add comments to pausable simulator

* add return type to isPaused

* add line

* fix comment

* simplify build script

* improve simulator export

* fix test file name

* Rename pausable.test.ts to Pausable.test.ts

* chore: initializable in pausable style (#34)

* move initializable and pausable to utils/

* remove barrel files, fix tests

* update initializable mock, sim, and tests

---------

Co-authored-by: Iskander <iskander.andrews@openzeppelin.com>
* bump midnight-js

* remove unused deps

* remove eslint and prettier, add biome

* run fmt

* run lint

* organize imports

* remove dev deps from contracts/

* remove unused deps

* update turbo and scripts

* add clean script

* add clean script

* clean up test config

* remove jest reporters

* bump turbo to 2.5

* bump turbo

* fix package names
* add security section and doc

* fix email

* Update README.md

Co-authored-by: Iskander <0xisk@proton.me>

* fix bare url

---------

Co-authored-by: Iskander <0xisk@proton.me>
* bump midnight-js

* remove unused deps

* remove eslint and prettier, add biome

* run fmt

* run lint

* organize imports

* remove dev deps from contracts/

* remove unused deps

* update turbo and scripts

* add clean script

* add clean script

* clean up test config

* remove jest reporters

* bump turbo to 2.5

* bump turbo

* fix package names

* set up compact compiler and builder

* update scripts

* update yarn.lock

* update readme

* fix fmt

* fix fmt

* remove lingering file

* update biome, fix fmt

* add requirements in dev section

* add devdeps to contracts packages

* simplify workspace

* remove unnecessary button rule

* fix fmt

* remove useExhaustiveDeps rule

* Uses recommended compiler options for Node 22 and TypeScript 5.8

* Update compact/src/Builder.ts

Co-authored-by: Iskander <0xisk@proton.me>

* remove author

* Apply suggestions from code review

Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>

* Update biome.json

Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>

* fix correctness and a11y

* remove implicit trailingCommas and indentWidth

* add stderr discard notice

* change dirent.path to parentPath

* add getCompactFiles method

* fix lint

* Improves type safety via custom error type

* output will never be null

* remove redundant check

* Colocate error types into their own file

* Adds type guard to `executeStep`

* Add type guard to `compileFile`

* Fix fmt

* update printOutput function signature

---------

Co-authored-by: Emanuel Solis <esolis6114@gmail.com>
Co-authored-by: Iskander <0xisk@proton.me>
Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>
* migrate to vitest, bump compact-runtime

* use vitest run, remove vitest ui dep

* add vitest imports for testing

* remove vitest ui

* bump compact-runtime to 0.8.1

* set vitest reporters to verbose

* fix fmt

* fix lint

* remove unused dep
Co-authored-by: Iskander <0xisk@proton.me>
Co-authored-by: Iskander <iskander.andrews@openzeppelin.com>
Co-authored-by: Emanuel Solis <esolis6114@gmail.com>
Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>
Signed-off-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>
Co-authored-by: Andrew Fleming <fleming-andrew@protonmail.com>
Signed-off-by: Andrew Fleming <fleming.andrew@protonmail.com>
Co-authored-by: Iskander <iskander.andrews@openzeppelin.com>
Co-authored-by: ⟣ €₥ℵ∪ℓ ⟢ <34749913+emnul@users.noreply.github.com>
@emnul emnul force-pushed the erc721-unshielded branch from 40cbe67 to 4256f72 Compare June 26, 2025 08:28
@andrew-fleming
Copy link
Contributor

Superseded by #158

@emnul emnul deleted the erc721-unshielded branch July 8, 2025 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request under-review Pull requests being reviewed by OpenZeppelin core team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Change ERC721 Name => NonFungibleToken

3 participants