-
Notifications
You must be signed in to change notification settings - Fork 4
ERC721: add verified foundation scaffold and proof bridges (#73) #559
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
Merged
Merged
Changes from all commits
Commits
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,43 @@ | ||
| /- | ||
| Compiler.Proofs.SpecCorrectness.ERC721 | ||
|
|
||
| Initial bridge theorem surface for ERC721 scaffolding. | ||
| -/ | ||
|
|
||
| import Verity.Specs.ERC721.Spec | ||
| import Verity.Examples.ERC721 | ||
| import Verity.Proofs.ERC721.Basic | ||
|
|
||
| namespace Compiler.Proofs.SpecCorrectness | ||
|
|
||
| open Verity | ||
| open Verity.Specs.ERC721 | ||
|
|
||
| /-- Spec/EDSL agreement for read-only `balanceOf`. -/ | ||
| theorem erc721_balanceOf_spec_correct (s : ContractState) (addr : Address) : | ||
| balanceOf_spec addr ((Verity.Examples.ERC721.balanceOf addr).runValue s) s := by | ||
| simpa using Verity.Proofs.ERC721.balanceOf_meets_spec s addr | ||
|
|
||
| /-- Spec/EDSL agreement for read-only `ownerOf`. -/ | ||
| theorem erc721_ownerOf_spec_correct (s : ContractState) (tokenId : Uint256) : | ||
| ownerOf_spec tokenId ((Verity.Examples.ERC721.ownerOf tokenId).runValue s) s := by | ||
| simpa using Verity.Proofs.ERC721.ownerOf_meets_spec s tokenId | ||
|
|
||
| /-- Spec/EDSL agreement for read-only `getApproved`. -/ | ||
| theorem erc721_getApproved_spec_correct (s : ContractState) (tokenId : Uint256) : | ||
| getApproved_spec tokenId ((Verity.Examples.ERC721.getApproved tokenId).runValue s) s := by | ||
| simpa using Verity.Proofs.ERC721.getApproved_meets_spec s tokenId | ||
|
|
||
| /-- Spec/EDSL agreement for read-only `isApprovedForAll`. -/ | ||
| theorem erc721_isApprovedForAll_spec_correct (s : ContractState) (ownerAddr operator : Address) : | ||
| isApprovedForAll_spec ownerAddr operator | ||
| ((Verity.Examples.ERC721.isApprovedForAll ownerAddr operator).runValue s) s := by | ||
| simpa using Verity.Proofs.ERC721.isApprovedForAll_meets_spec s ownerAddr operator | ||
|
|
||
| /-- Spec/EDSL agreement for `setApprovalForAll` with sender-bound owner. -/ | ||
| theorem erc721_setApprovalForAll_spec_correct (s : ContractState) (operator : Address) (approved : Bool) : | ||
| setApprovalForAll_spec s.sender operator approved s | ||
| ((Verity.Examples.ERC721.setApprovalForAll operator approved).runState s) := by | ||
| simpa using Verity.Proofs.ERC721.setApprovalForAll_meets_spec s operator approved | ||
|
|
||
| end Compiler.Proofs.SpecCorrectness |
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
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
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,28 @@ | ||
| /- | ||
| Verity.AST.ERC721: initial AST bridge scaffold. | ||
|
|
||
| This file establishes the AST/denotation seam for ERC721 foundation work. | ||
| -/ | ||
|
|
||
| import Verity.Denote | ||
| import Verity.Examples.ERC721 | ||
|
|
||
| namespace Verity.AST.ERC721 | ||
|
|
||
| open Verity | ||
| open Verity.AST | ||
| open Verity.Denote | ||
| open Verity.Examples.ERC721 (balanceOf) | ||
|
|
||
| /-- AST for `balanceOf(addr)`: return mapping slot3[addr]. -/ | ||
| def balanceOfAST : Stmt := | ||
| .bindUint "x" (.mapping 3 (.varAddr "addr")) | ||
| (.ret (.var "x")) | ||
|
|
||
| /-- `balanceOf` AST denotes to the EDSL `balanceOf` function. -/ | ||
| theorem balanceOf_equiv (addr : Address) : | ||
| denoteUint emptyEnv (fun s => if s == "addr" then addr else 0) balanceOfAST | ||
| = balanceOf addr := by | ||
| rfl | ||
|
|
||
| end Verity.AST.ERC721 |
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
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,132 @@ | ||
| /- | ||
| ERC721 (foundation scaffold): | ||
| - balances mapping | ||
| - token ownership by tokenId | ||
| - token approvals and operator approvals | ||
|
|
||
| This module provides a compile-safe executable baseline for issue #73. | ||
| -/ | ||
|
|
||
| import Verity.Core | ||
| import Verity.EVM.Uint256 | ||
| import Verity.Stdlib.Math | ||
|
|
||
| namespace Verity.Examples.ERC721 | ||
|
|
||
| open Verity | ||
| open Verity.EVM.Uint256 | ||
| open Verity.Stdlib.Math | ||
|
|
||
| -- Storage layout | ||
| def owner : StorageSlot Address := ⟨0⟩ | ||
| def totalSupply : StorageSlot Uint256 := ⟨1⟩ | ||
| def nextTokenId : StorageSlot Uint256 := ⟨2⟩ | ||
| def balances : StorageSlot (Address → Uint256) := ⟨3⟩ | ||
| def owners : StorageSlot (Uint256 → Uint256) := ⟨4⟩ | ||
| def tokenApprovals : StorageSlot (Uint256 → Uint256) := ⟨5⟩ | ||
| def operatorApprovals : StorageSlot (Address → Address → Uint256) := ⟨6⟩ | ||
|
|
||
| def addressToWord (a : Address) : Uint256 := | ||
| (a.toNat : Uint256) | ||
|
|
||
| def wordToAddress (w : Uint256) : Address := | ||
| Verity.Core.Address.ofNat (w : Nat) | ||
|
|
||
| def boolToWord (b : Bool) : Uint256 := | ||
| if b then 1 else 0 | ||
|
|
||
| def isOwner : Contract Bool := do | ||
| let sender ← msgSender | ||
| let currentOwner ← getStorageAddr owner | ||
| return sender == currentOwner | ||
|
|
||
| def onlyOwner : Contract Unit := do | ||
| let ownerCheck ← isOwner | ||
| require ownerCheck "Caller is not the owner" | ||
|
|
||
| -- Constructor initializes owner and zeroes core counters. | ||
| def constructor (initialOwner : Address) : Contract Unit := do | ||
| setStorageAddr owner initialOwner | ||
| setStorage totalSupply 0 | ||
| setStorage nextTokenId 0 | ||
|
|
||
| -- Core ERC721 view helpers. | ||
| def balanceOf (addr : Address) : Contract Uint256 := do | ||
| getMapping balances addr | ||
|
|
||
| def ownerOf (tokenId : Uint256) : Contract Address := do | ||
| let ownerWord ← getMappingUint owners tokenId | ||
| return wordToAddress ownerWord | ||
|
|
||
| def getApproved (tokenId : Uint256) : Contract Address := do | ||
| let approvedWord ← getMappingUint tokenApprovals tokenId | ||
| return wordToAddress approvedWord | ||
|
|
||
| def isApprovedForAll (ownerAddr operator : Address) : Contract Bool := do | ||
| let flag ← getMapping2 operatorApprovals ownerAddr operator | ||
| return flag != 0 | ||
|
|
||
| -- Approve a specific address for a single tokenId. | ||
| def approve (approved : Address) (tokenId : Uint256) : Contract Unit := do | ||
| let sender ← msgSender | ||
| let tokenOwner ← ownerOf tokenId | ||
| require (sender == tokenOwner) "Not token owner" | ||
| setMappingUint tokenApprovals tokenId (addressToWord approved) | ||
|
|
||
| -- Set or clear global operator approval for sender. | ||
| def setApprovalForAll (operator : Address) (approved : Bool) : Contract Unit := do | ||
| let sender ← msgSender | ||
| setMapping2 operatorApprovals sender operator (boolToWord approved) | ||
|
|
||
| -- Owner-only mint with sequential token IDs. | ||
| def mint (to : Address) : Contract Uint256 := do | ||
| onlyOwner | ||
| require (to != 0) "Invalid recipient" | ||
|
|
||
| let tokenId ← getStorage nextTokenId | ||
| let currentOwnerWord ← getMappingUint owners tokenId | ||
| require (currentOwnerWord == 0) "Token already minted" | ||
|
|
||
| let recipientBalance ← getMapping balances to | ||
| let newRecipientBalance ← requireSomeUint (safeAdd recipientBalance 1) "Balance overflow" | ||
|
|
||
| let currentSupply ← getStorage totalSupply | ||
| let newSupply ← requireSomeUint (safeAdd currentSupply 1) "Supply overflow" | ||
|
|
||
| setMappingUint owners tokenId (addressToWord to) | ||
| setMapping balances to newRecipientBalance | ||
| setStorage totalSupply newSupply | ||
| setStorage nextTokenId (add tokenId 1) | ||
| return tokenId | ||
|
|
||
| -- Transfer token from owner to recipient with approval checks. | ||
| def transferFrom (fromAddr to : Address) (tokenId : Uint256) : Contract Unit := do | ||
| let sender ← msgSender | ||
| require (to != 0) "Invalid recipient" | ||
|
|
||
| let ownerWord ← getMappingUint owners tokenId | ||
| require (ownerWord != 0) "Token does not exist" | ||
|
|
||
| let tokenOwner := wordToAddress ownerWord | ||
| require (tokenOwner == fromAddr) "From is not owner" | ||
|
|
||
| let approvedWord ← getMappingUint tokenApprovals tokenId | ||
| let operatorWord ← getMapping2 operatorApprovals fromAddr sender | ||
| let senderWord := addressToWord sender | ||
| let authorized := sender == fromAddr || approvedWord == senderWord || operatorWord != 0 | ||
| require authorized "Not authorized" | ||
|
|
||
| if fromAddr == to then | ||
| pure () | ||
| else | ||
| let fromBalance ← getMapping balances fromAddr | ||
| require (fromBalance >= 1) "Insufficient balance" | ||
| let toBalance ← getMapping balances to | ||
| let newToBalance ← requireSomeUint (safeAdd toBalance 1) "Balance overflow" | ||
| setMapping balances fromAddr (sub fromBalance 1) | ||
| setMapping balances to newToBalance | ||
|
|
||
| setMappingUint owners tokenId (addressToWord to) | ||
| setMappingUint tokenApprovals tokenId 0 | ||
|
|
||
| end Verity.Examples.ERC721 | ||
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.
Wrapping add for nextTokenId inconsistent with safeAdd pattern
Low Severity
The
mintfunction usessafeAddwithrequireSomeUintfor bothrecipientBalanceandtotalSupplyto guard against overflow, but uses wrappingaddfornextTokenId. Every othermintfunction in the codebase (ERC20,SimpleToken) consistently usessafeAddfor all counter increments. While currently safe because thesequential_supplyinvariant keeps both values equal andtotalSupply'ssafeAddreverts first, this inconsistency is fragile — if burn functionality is ever added (decreasing supply but notnextTokenId), the wrappingaddcould silently wrapnextTokenIdto zero.