forked from vyperlang/vyper
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: erc1155 example w/ ownable/pausable (vyperlang#2807)
* Created ERC1155 ownable example/template * Applied most of proposed changes * Applied more of the feedback. * ERC165 fix * Applied @view/@pure, updated test to accomodate * Moved tests into the correct folder * added interface ERC1155.py * refactor: clean up built-in interface * rewrote tests to use eth-tester * cleaned up brownie tests in favor of eth-tester * Update examples/tokens/ERC1155ownable.vy Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> * applied proposed changes in contract and tests * interface fix / callback bytes constant * Ownership test assertion to ZERO_ADDRESS * reworked uri part, simplified tests * Fixed all testscript issues * refactor: apply suggestions from code review * Update examples/tokens/ERC1155ownable.vy Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> * Update examples/tokens/ERC1155ownable.vy Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> * applied codebase shrink tips, adjusted tests * cleaning up commented out code * refactor: flip orientation of `balanceOf` * test: update tests for switch of args to `balanceOf` * pull, isort, black, push * Resolved linter issues * Linter fixes for ERC1155 test script * Fixed ownership check in safeTransferFrom * Fixed setURI permissions, updated to docstrings * Minor test update Co-authored-by: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com>
- Loading branch information
Showing
2 changed files
with
731 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,367 @@ | ||
# @version >=0.3.3 | ||
""" | ||
@dev Implementation of ERC-1155 non-fungible token standard ownable, with approval, OPENSEA compatible (name, symbol) | ||
@author Dr. Pixel (github: @Doc-Pixel) | ||
""" | ||
############### imports ############### | ||
from vyper.interfaces import ERC165 | ||
|
||
############### variables ############### | ||
# maximum items in a batch call. Set to 128, to be determined what the practical limits are. | ||
BATCH_SIZE: constant(uint256) = 128 | ||
|
||
# callback number of bytes | ||
CALLBACK_NUMBYTES: constant(uint256) = 4096 | ||
|
||
# URI length set to 1024. | ||
MAX_URI_LENGTH: constant(uint256) = 1024 | ||
|
||
# the contract owner | ||
# not part of the core spec but a common feature for NFT projects | ||
owner: public(address) | ||
|
||
# pause status True / False | ||
# not part of the core spec but a common feature for NFT projects | ||
paused: public(bool) | ||
|
||
# the contracts URI to find the metadata | ||
_uri: String[MAX_URI_LENGTH] | ||
|
||
# NFT marketplace compatibility | ||
name: public(String[128]) | ||
symbol: public(String[16]) | ||
|
||
# Interface IDs | ||
ERC165_INTERFACE_ID: constant(bytes4) = 0x01ffc9a7 | ||
ERC1155_INTERFACE_ID: constant(bytes4) = 0xd9b67a26 | ||
ERC1155_INTERFACE_ID_METADATA: constant(bytes4) = 0x0e89341c | ||
|
||
# mappings | ||
|
||
# Mapping from token ID to account balances | ||
balanceOf: public(HashMap[address, HashMap[uint256, uint256]]) | ||
|
||
# Mapping from account to operator approvals | ||
isApprovedForAll: public( HashMap[address, HashMap[address, bool]]) | ||
|
||
############### events ############### | ||
event Paused: | ||
# Emits a pause event with the address that paused the contract | ||
account: address | ||
|
||
event unPaused: | ||
# Emits an unpause event with the address that paused the contract | ||
account: address | ||
|
||
event OwnershipTransferred: | ||
# Emits smart contract ownership transfer from current to new owner | ||
previouwOwner: address | ||
newOwner: address | ||
|
||
event TransferSingle: | ||
# Emits on transfer of a single token | ||
operator: indexed(address) | ||
fromAddress: indexed(address) | ||
to: indexed(address) | ||
id: uint256 | ||
value: uint256 | ||
|
||
event TransferBatch: | ||
# Emits on batch transfer of tokens. the ids array correspond with the values array by their position | ||
operator: indexed(address) # indexed | ||
fromAddress: indexed(address) | ||
to: indexed(address) | ||
ids: DynArray[uint256, BATCH_SIZE] | ||
values: DynArray[uint256, BATCH_SIZE] | ||
|
||
event ApprovalForAll: | ||
# This emits when an operator is enabled or disabled for an owner. The operator manages all tokens for an owner | ||
account: indexed(address) | ||
operator: indexed(address) | ||
approved: bool | ||
|
||
event URI: | ||
# This emits when the URI gets changed | ||
value: String[MAX_URI_LENGTH] | ||
id: uint256 | ||
|
||
|
||
############### interfaces ############### | ||
implements: ERC165 | ||
|
||
interface IERC1155Receiver: | ||
def onERC1155Received( | ||
operator: address, | ||
sender: address, | ||
id: uint256, | ||
amount: uint256, | ||
data: Bytes[CALLBACK_NUMBYTES], | ||
) -> bytes32: payable | ||
def onERC1155BatchReceived( | ||
operator: address, | ||
sender: address, | ||
ids: DynArray[uint256, BATCH_SIZE], | ||
amounts: DynArray[uint256, BATCH_SIZE], | ||
data: Bytes[CALLBACK_NUMBYTES], | ||
) -> bytes4: payable | ||
|
||
interface IERC1155MetadataURI: | ||
def uri(id: uint256) -> String[MAX_URI_LENGTH]: view | ||
|
||
############### functions ############### | ||
|
||
@external | ||
def __init__(name: String[128], symbol: String[16], uri: String[1024]): | ||
""" | ||
@dev contract initialization on deployment | ||
@dev will set name and symbol, interfaces, owner and URI | ||
@dev self.paused will default to false | ||
@param name the smart contract name | ||
@param symbol the smart contract symbol | ||
@param uri the new uri for the contract | ||
""" | ||
self.name = name | ||
self.symbol = symbol | ||
self.owner = msg.sender | ||
self._uri = uri | ||
|
||
## contract status ## | ||
@external | ||
def pause(): | ||
""" | ||
@dev Pause the contract, checks if the caller is the owner and if the contract is paused already | ||
@dev emits a pause event | ||
@dev not part of the core spec but a common feature for NFT projects | ||
""" | ||
assert self.owner == msg.sender, "Ownable: caller is not the owner" | ||
assert not self.paused, "the contract is already paused" | ||
self.paused = True | ||
log Paused(msg.sender) | ||
|
||
@external | ||
def unpause(): | ||
""" | ||
@dev Unpause the contract, checks if the caller is the owner and if the contract is paused already | ||
@dev emits an unpause event | ||
@dev not part of the core spec but a common feature for NFT projects | ||
""" | ||
assert self.owner == msg.sender, "Ownable: caller is not the owner" | ||
assert self.paused, "the contract is not paused" | ||
self.paused = False | ||
log unPaused(msg.sender) | ||
|
||
## ownership ## | ||
@external | ||
def transferOwnership(newOwner: address): | ||
""" | ||
@dev Transfer the ownership. Checks for contract pause status, current owner and prevent transferring to | ||
@dev zero address | ||
@dev emits an OwnershipTransferred event with the old and new owner addresses | ||
@param newOwner The address of the new owner. | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert self.owner == msg.sender, "Ownable: caller is not the owner" | ||
assert newOwner != self.owner, "This account already owns the contract" | ||
assert newOwner != ZERO_ADDRESS, "Transfer to ZERO_ADDRESS not allowed. Use renounceOwnership() instead." | ||
oldOwner: address = self.owner | ||
self.owner = newOwner | ||
log OwnershipTransferred(oldOwner, newOwner) | ||
|
||
@external | ||
def renounceOwnership(): | ||
""" | ||
@dev Transfer the ownership to ZERO_ADDRESS, this will lock the contract | ||
@dev emits an OwnershipTransferred event with the old and new ZERO_ADDRESS owner addresses | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert self.owner == msg.sender, "Ownable: caller is not the owner" | ||
oldOwner: address = self.owner | ||
self.owner = ZERO_ADDRESS | ||
log OwnershipTransferred(oldOwner, ZERO_ADDRESS) | ||
|
||
@external | ||
@view | ||
def balanceOfBatch(accounts: DynArray[address, BATCH_SIZE], ids: DynArray[uint256, BATCH_SIZE]) -> DynArray[uint256,BATCH_SIZE]: # uint256[BATCH_SIZE]: | ||
""" | ||
@dev check the balance for an array of specific IDs and addresses | ||
@dev will return an array of balances | ||
@dev Can also be used to check ownership of an ID | ||
@param accounts a dynamic array of the addresses to check the balance for | ||
@param ids a dynamic array of the token IDs to check the balance | ||
""" | ||
assert len(accounts) == len(ids), "ERC1155: accounts and ids length mismatch" | ||
batchBalances: DynArray[uint256, BATCH_SIZE] = [] | ||
j: uint256 = 0 | ||
for i in ids: | ||
batchBalances.append(self.balanceOf[accounts[j]][i]) | ||
j += 1 | ||
return batchBalances | ||
|
||
## mint ## | ||
@external | ||
def mint(receiver: address, id: uint256, amount:uint256, data:bytes32): | ||
""" | ||
@dev mint one new token with a certain ID | ||
@dev this can be a new token or "topping up" the balance of a non-fungible token ID | ||
@param receiver the account that will receive the minted token | ||
@param id the ID of the token | ||
@param amount of tokens for this ID | ||
@param data the data associated with this mint. Usually stays empty | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert self.owner == msg.sender, "Only the contract owner can mint" | ||
assert receiver != ZERO_ADDRESS, "Can not mint to ZERO ADDRESS" | ||
operator: address = msg.sender | ||
self.balanceOf[receiver][id] += amount | ||
log TransferSingle(operator, ZERO_ADDRESS, receiver, id, amount) | ||
|
||
|
||
@external | ||
def mintBatch(receiver: address, ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE], data: bytes32): | ||
""" | ||
@dev mint a batch of new tokens with the passed IDs | ||
@dev this can be new tokens or "topping up" the balance of existing non-fungible token IDs in the contract | ||
@param receiver the account that will receive the minted token | ||
@param ids array of ids for the tokens | ||
@param amounts amounts of tokens for each ID in the ids array | ||
@param data the data associated with this mint. Usually stays empty | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert self.owner == msg.sender, "Only the contract owner can mint" | ||
assert receiver != ZERO_ADDRESS, "Can not mint to ZERO ADDRESS" | ||
assert len(ids) == len(amounts), "ERC1155: ids and amounts length mismatch" | ||
operator: address = msg.sender | ||
|
||
for i in range(BATCH_SIZE): | ||
if i >= len(ids): | ||
break | ||
self.balanceOf[receiver][ids[i]] += amounts[i] | ||
|
||
log TransferBatch(operator, ZERO_ADDRESS, receiver, ids, amounts) | ||
|
||
## burn ## | ||
@external | ||
def burn(id: uint256, amount: uint256): | ||
""" | ||
@dev burn one or more token with a certain ID | ||
@dev the amount of tokens will be deducted from the holder's balance | ||
@param receiver the account that will receive the minted token | ||
@param id the ID of the token to burn | ||
@param amount of tokens to burnfor this ID | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert self.balanceOf[msg.sender][id] > 0 , "caller does not own this ID" | ||
self.balanceOf[msg.sender][id] -= amount | ||
log TransferSingle(msg.sender, msg.sender, ZERO_ADDRESS, id, amount) | ||
|
||
@external | ||
def burnBatch(ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE]): | ||
""" | ||
@dev burn a batch of tokens with the passed IDs | ||
@dev this can be burning non fungible tokens or reducing the balance of existing non-fungible token IDs in the contract | ||
@dev inside the loop ownership will be checked for each token. We can not burn tokens we do not own | ||
@param ids array of ids for the tokens to burn | ||
@param amounts array of amounts of tokens for each ID in the ids array | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert len(ids) == len(amounts), "ERC1155: ids and amounts length mismatch" | ||
operator: address = msg.sender | ||
|
||
for i in range(BATCH_SIZE): | ||
if i >= len(ids): | ||
break | ||
self.balanceOf[msg.sender][ids[i]] -= amounts[i] | ||
|
||
log TransferBatch(msg.sender, msg.sender, ZERO_ADDRESS, ids, amounts) | ||
|
||
## approval ## | ||
@external | ||
def setApprovalForAll(owner: address, operator: address, approved: bool): | ||
""" | ||
@dev set an operator for a certain NFT owner address | ||
@param account the NFT owner address | ||
@param operator the operator address | ||
""" | ||
assert owner == msg.sender, "You can only set operators for your own account" | ||
assert not self.paused, "The contract has been paused" | ||
assert owner != operator, "ERC1155: setting approval status for self" | ||
self.isApprovedForAll[owner][operator] = approved | ||
log ApprovalForAll(owner, operator, approved) | ||
|
||
@external | ||
def safeTransferFrom(sender: address, receiver: address, id: uint256, amount: uint256, bytes: bytes32): | ||
""" | ||
@dev transfer token from one address to another. | ||
@param sender the sending account (current owner) | ||
@param receiver the receiving account | ||
@param id the token id that will be sent | ||
@param amount the amount of tokens for the specified id | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert receiver != ZERO_ADDRESS, "ERC1155: transfer to the zero address" | ||
assert sender != receiver | ||
assert sender == msg.sender or self.isApprovedForAll[sender][msg.sender], "Caller is neither owner nor approved operator for this ID" | ||
assert self.balanceOf[sender][id] > 0 , "caller does not own this ID or ZERO balance" | ||
operator: address = msg.sender | ||
self.balanceOf[sender][id] -= amount | ||
self.balanceOf[receiver][id] += amount | ||
log TransferSingle(operator, sender, receiver, id, amount) | ||
|
||
@external | ||
def safeBatchTransferFrom(sender: address, receiver: address, ids: DynArray[uint256, BATCH_SIZE], amounts: DynArray[uint256, BATCH_SIZE], _bytes: bytes32): | ||
""" | ||
@dev transfer tokens from one address to another. | ||
@param sender the sending account | ||
@param receiver the receiving account | ||
@param ids a dynamic array of the token ids that will be sent | ||
@param amounts a dynamic array of the amounts for the specified list of ids. | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert receiver != ZERO_ADDRESS, "ERC1155: transfer to the zero address" | ||
assert sender != receiver | ||
assert sender == msg.sender or self.isApprovedForAll[sender][msg.sender], "Caller is neither owner nor approved operator for this ID" | ||
assert len(ids) == len(amounts), "ERC1155: ids and amounts length mismatch" | ||
operator: address = msg.sender | ||
for i in range(BATCH_SIZE): | ||
if i >= len(ids): | ||
break | ||
id: uint256 = ids[i] | ||
amount: uint256 = amounts[i] | ||
self.balanceOf[sender][id] -= amount | ||
self.balanceOf[receiver][id] += amount | ||
|
||
log TransferBatch(operator, sender, receiver, ids, amounts) | ||
|
||
# URI # | ||
@external | ||
def setURI(uri: String[MAX_URI_LENGTH]): | ||
""" | ||
@dev set the URI for the contract | ||
@param uri the new uri for the contract | ||
""" | ||
assert not self.paused, "The contract has been paused" | ||
assert self._uri != uri, "new and current URI are identical" | ||
assert msg.sender == self.owner, "Only the contract owner can update the URI" | ||
self._uri = uri | ||
log URI(uri, 0) | ||
|
||
@external | ||
def uri(id: uint256) -> String[MAX_URI_LENGTH]: | ||
""" | ||
@dev retrieve the uri, this function can optionally be extended to return dynamic uris. out of scope. | ||
@param id NFT ID to retrieve the uri for. | ||
""" | ||
return self._uri | ||
|
||
@pure | ||
@external | ||
def supportsInterface(interfaceId: bytes4) -> bool: | ||
""" | ||
@dev Returns True if the interface is supported | ||
@param interfaceID bytes4 interface identifier | ||
""" | ||
return interfaceId in [ | ||
ERC165_INTERFACE_ID, | ||
ERC1155_INTERFACE_ID, | ||
ERC1155_INTERFACE_ID_METADATA, | ||
] |
Oops, something went wrong.