Conversation
0xisk
left a comment
There was a problem hiding this comment.
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.
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 |
andrew-fleming
left a comment
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
contracts/erc721/src/ERC721.compact
Outdated
There was a problem hiding this comment.
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:
-
Safe‐only circuit
- Signature:
transfer(to: Either<ZswapCoinPublicKey, ContractAddress>, amount: Uint128) - Behavior: if
tois a contract, revert immediately; iftois a public key, do the transfer.
- Signature:
-
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:
-
Deprecation phase (minor version):
- Mark
_unsafe_transferas deprecated in the docs and emit a warning if possible. - Keep its implementation intact so existing callers continue to work.
- Mark
-
Major release:
- Drop
_unsafe_transferand ensuretransferhas noisContractguard. - By this point, anyone using
_unsafe_transfershould have migrated to the now C2C-capabletransfer.
- Drop
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).
There was a problem hiding this comment.
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_transferFromto bypass the “reject contract” check.
- This is the main public transfer. As soon as you allow
-
_transfer(from: Either<…>, to: Either<…>, tokenId: Uint<128>)- Internal direct‐transfer helper. If you switch its
totype toEither<…>, add_unsafe_transferso it can send into contracts.
- Internal direct‐transfer helper. If you switch its
-
_mint(to: Either<…>, tokenId: Uint<128>)- Minting is effectively “transfer from zero.” If you allow
to=Either<…>, you must have_unsafe_mintto let a contract receive an NFT (knowing it can’t send back until C2C exists).
- Minting is effectively “transfer from zero.” If you allow
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.
There was a problem hiding this comment.
Thanks for this @0xisk! We should provide this migration plan more formally in our docs
8ab6b72 to
8605323
Compare
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 |
andrew-fleming
left a comment
There was a problem hiding this comment.
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
contracts/erc721/src/ERC721.compact
Outdated
There was a problem hiding this comment.
Thanks for this @0xisk! We should provide this migration plan more formally in our docs
* 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>
|
Superseded by #158 |
Brings an implementation of an ERC721-like specification to Compact
This will not compile unless you use
compactcv0.23.0Closes #123 #6 #129