diff --git a/README.md b/README.md index f2997d45..0400160a 100644 --- a/README.md +++ b/README.md @@ -367,26 +367,27 @@ The following methods and events are applicable to Fungible Tokens. The following methods and events are applicable to Non-Fungible Tokens. - - -| Function | Comment | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `approve(address _approved, uint256 _tokenId) payable` | Change or reaffirm the approved address for an NFT. The zero address indicates there is no approved address. Throws unless `msg.sender` is the current NFT owner, or an authorized operator of the current owner. | -| `balanceOf(address _owner) view` | Count all NFTs assigned to an owner. NFTs assigned to the zero address are considered invalid, and this function throws for queries about the zero address. | -| `getApproved(uint256 _tokenId) view` | Get the approved address for a single NFT. Throws if `_tokenId` is not a valid NFT. | -| `isApprovedForAll(address _owner, address _operator) view` | Query if an address is an authorized operator for another address | -| `ownerOf(uint256 _tokenId) view` | Find the owner of an NFT. NFTs assigned to zero address are considered invalid, and queries about them do throw. | -| `safeTransferFrom(address _from, address _to, uint256 _tokenId) payable` | Transfers the ownership of an NFT from one address to another address. This works identically to the other function with an extra data parameter, except this function just sets data to "". | -| `safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) payable` | Transfers the ownership of an NFT from one address to another address. Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for this NFT. Throws if `_from` is not the current owner. Throws if `_to` is the zero address. Throws if `_tokenId` is not a valid NFT. When transfer is complete, this function checks if `_to` is a smart contract (code size > 0). If so, it calls `onERC721Received` on `_to` and throws if the return value is not `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. | -| `setApprovalForAll(address _operator, bool _approved)` | Enable or disable approval for a third party ("operator") to manage all of `msg.sender`'s assets. Emits the ApprovalForAll event. The contract MUST allow multiple operators per owner. | -| `supportsInterface(bytes4 interfaceID) view` | Query if a contract implements an interface. Interface identification is specified in ERC-165. This function uses less than 30,000 gas. | -| `transferFrom(address _from, address _to, uint256 _tokenId) payable` | Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE THEY MAY BE PERMANENTLY LOST. Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for this NFT. Throws if `_from` is not the current owner. Throws if `_to` is the zero address. Throws if `_tokenId` is not a valid NFT. | - -| Event | Comment | -| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId)` | This emits when the approved address for an NFT is changed or reaffirmed. The zero address indicates there is no approved address. When a Transfer event emits, this also indicates that the approved address for that NFT (if any) is reset to none. | -| `ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved)` | This emits when an operator is enabled or disabled for an owner. The operator can manage all NFTs of the owner. | -| `Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId)` | This emits when ownership of any NFT changes by any mechanism. This event emits when NFTs are created (`from` == 0) and destroyed (`to` == 0). Exception: during contract creation, any number of NFTs may be created and assigned without emitting Transfer. At the time of any transfer, the approved address for that NFT (if any) is reset to none. | + + +| Function | Comment | +| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `approve(address spender, uint256 serialId) payable` | Gives permission to `spender` to transfer `serialId` token to another account. The approval is cleared when the token is transferred. Only a single account can be approved at a time, so approving the zero address clears previous approvals. Requirements: - The caller must own the token or be an approved operator. - `serialId` must exist. Emits an {Approval} event. | +| `balanceOf(address owner) view` | Returns the number of tokens in `owner`'s account. | +| `getApproved(uint256 serialId) view` | Returns the account approved for `serialId` token. Requirements: - `serialId` must exist. | +| `isApprovedForAll(address owner, address operator) view` | Returns if the `operator` is allowed to manage all of the assets of `owner`. See {setApprovalForAll} | +| `name() view` | Returns the token collection name. | +| `ownerOf(uint256 serialId) view` | Returns the owner of the `serialId` token. Requirements: - `serialId` must exist. This value changes when {approve} or {transferFrom} are called. | +| `setApprovalForAll(address operator, bool approved)` | Approve or remove `operator` as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. Requirements: - The `operator` cannot be the address zero. Emits an {ApprovalForAll} event. | +| `symbol() view` | Returns the token collection symbol. | +| `tokenURI(uint256 serialId) view` | Returns the Uniform Resource Identifier (URI) for `serialId` token. | +| `totalSupply() view` | Returns the total amount of tokens stored by the contract. | +| `transferFrom(address sender, address recipient, uint256 serialId) payable` | Transfers `serialId` token from `sender` to `recipient`. Requirements: - `sender` cannot be the zero address. - `recipient` cannot be the zero address. - `serialId` token must be owned by `sender`. - If the caller is not `sender`, it must be approved to move this token by either {approve} or {setApprovalForAll}. Emits a {Transfer} event. | + +| Event | Comment | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)` | Emitted when the approved address for an NFT is changed or reaffirmed from {from} to {to} address. The zero {to} address indicates there will be no approved address. Additionally the approved address for that NFT (if any) is reset to none. | +| `ApprovalForAll(address indexed owner, address indexed operator, bool approved)` | Emitted when an operator {operator} is enabled or disabled {approved} for an owner {owner}. The operator {operator} can than manage all NFTs of the owner {owner}. | +| `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)` | Emitted when ownership of any NFT changes by any mechanism. This event emits when NFTs are created (`from` == 0) and destroyed (`to` == 0). Otherwise, it indicates that the token with ID {tokenId} was transferred from {from} to {to}, where {from} represents the previous owner of the token, not the approved spender. Exception: during contract creation, any number of NFTs may be created and assigned without emitting Transfer. At the time of any transfer, the approved address for that NFT (if any) is reset to none. | @@ -408,11 +409,36 @@ The following methods can be invoked on the Hedera Token Service contract locate -| Function | Comment | -| --------------------------------------------------------------- | -------------------------------------------------------------- | -| `burnToken(address token, int64 amount, int64[] serialNumbers)` | Burns an amount of the token from the defined treasury account | -| `getTokenInfo(address token)` | Query token info | -| `mintToken(address token, int64 amount, bytes[] metadata)` | Mints an amount of the token to the defined treasury account | +| Function | Comment | +| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowance(address token, address owner, address spender)` | Returns the amount which spender is still allowed to withdraw from owner. Only Applicable to Fungible Tokens | +| `approve(address token, address spender, uint256 amount)` | Allows spender to withdraw from your account multiple times, up to the value amount. If this function is called again it overwrites the current allowance with value. Only Applicable to Fungible Tokens | +| `approveNFT(address token, address approved, uint256 serialNumber)` | Allow or reaffirm the approved address to transfer an NFT the approved address does not own. Only Applicable to NFT Tokens | +| `associateToken(address account, address token)` | Single-token variant of associateTokens. Will be mapped to a single entry array call of associateTokens | +| `associateTokens(address account, address[] tokens)` | Associates the provided account with the provided tokens. Must be signed by the provided Account's key or called from the accounts contract key If the provided account is not found, the transaction will resolve to INVALID_ACCOUNT_ID. If the provided account has been deleted, the transaction will resolve to ACCOUNT_DELETED. If any of the provided tokens is not found, the transaction will resolve to INVALID_TOKEN_REF. If any of the provided tokens has been deleted, the transaction will resolve to TOKEN_WAS_DELETED. If an association between the provided account and any of the tokens already exists, the transaction will resolve to TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT. If the provided account's associations count exceed the constraint of maximum token associations per account, the transaction will resolve to TOKENS_PER_ACCOUNT_LIMIT_EXCEEDED. On success, associations between the provided account and tokens are made and the account is ready to interact with the tokens. | +| `burnToken(address token, int64 amount, int64[] serialNumbers)` | Burns an amount of the token from the defined treasury account | +| `dissociateToken(address account, address token)` | Single-token variant of dissociateTokens. Will be mapped to a single entry array call of dissociateTokens | +| `dissociateTokens(address account, address[] tokens)` | Dissociates the provided account with the provided tokens. Must be signed by the provided Account's key. If the provided account is not found, the transaction will resolve to INVALID_ACCOUNT_ID. If the provided account has been deleted, the transaction will resolve to ACCOUNT_DELETED. If any of the provided tokens is not found, the transaction will resolve to INVALID_TOKEN_REF. If any of the provided tokens has been deleted, the transaction will resolve to TOKEN_WAS_DELETED. If an association between the provided account and any of the tokens does not exist, the transaction will resolve to TOKEN_NOT_ASSOCIATED_TO_ACCOUNT. If a token has not been deleted and has not expired, and the user has a nonzero balance, the transaction will resolve to TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES. If a fungible token has expired, the user can disassociate even if their token balance is not zero. If a non fungible token has expired, the user can not disassociate if their token balance is not zero. The transaction will resolve to TRANSACTION_REQUIRED_ZERO_TOKEN_BALANCES. On success, associations between the provided account and tokens are removed. | +| `getApproved(address token, uint256 serialNumber)` | Get the approved address for a single NFT Only Applicable to NFT Tokens | +| `getFungibleTokenInfo(address token)` | Query fungible token info | +| `getNonFungibleTokenInfo(address token, int64 serialNumber)` | Query non fungible token info | +| `getTokenCustomFees(address token)` | Query token custom fees | +| `getTokenDefaultFreezeStatus(address token)` | Query token default freeze status | +| `getTokenDefaultKycStatus(address token)` | Query token default kyc status | +| `getTokenExpiryInfo(address token)` | Query token expiry info | +| `getTokenInfo(address token)` | Query token info | +| `getTokenKey(address token, uint256 keyType)` | Query token KeyValue | +| `getTokenType(address token)` | Query to return the token type for a given address | +| `isApprovedForAll(address token, address owner, address operator)` | Query if an address is an authorized operator for another address Only Applicable to NFT Tokens | +| `isToken(address token)` | Query if valid token found for the given address | +| `mintToken(address token, int64 amount, bytes[] metadata)` | Mints an amount of the token to the defined treasury account | +| `setApprovalForAll(address token, address operator, bool approved)` | Enable or disable approval for a third party ("operator") to manage all of `msg.sender`'s assets | +| `transferFrom(address token, address from, address to, uint256 amount)` | Only applicable to fungible tokens | +| `transferFromNFT(address token, address from, address to, uint256 serialNumber)` | Transfers `serialNumber` of `token` from `from` to `to` using the allowance mechanism. Only applicable to NFT tokens | +| `transferNFT(address token, address sender, address recipient, int64 serialNumber)` | Transfers tokens where the calling account/contract is implicitly the first entry in the token transfer list, where the amount is the value needed to zero balance the transfers. Regular signing rules apply for sending (positive amount) or receiving (negative amount) | +| `transferNFTs(address token, address[] sender, address[] receiver, int64[] serialNumber)` | Initiates a Non-Fungable Token Transfer | +| `transferToken(address token, address sender, address recipient, int64 amount)` | Transfers tokens where the calling account/contract is implicitly the first entry in the token transfer list, where the amount is the value needed to zero balance the transfers. Regular signing rules apply for sending (positive amount) or receiving (negative amount) | +| `transferTokens(address token, address[] accountId, int64[] amount)` | Initiates a Fungible Token Transfer | diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index 4dcaaf6c..4a897b8b 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -10,7 +10,7 @@ import {SetTokenInfo} from "./SetTokenInfo.sol"; address constant HTS_ADDRESS = address(0x167); -contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { +contract HtsSystemContract is IHederaTokenService { /** * The slot's value contains the next token ID to use when a token is being created. @@ -574,7 +574,7 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { address to = address(bytes20(msg.data[72:92])); uint256 amount = uint256(bytes32(msg.data[92:124])); _approve(from, to, amount); - emit Approval(from, to, amount); + emit IERC20Events.Approval(from, to, amount); return abi.encode(true); } if (selector == this.approveNFT.selector) { @@ -670,7 +670,7 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { uint256 amount = uint256(bytes32(msg.data[60:92])); address owner = msg.sender; _approve(owner, spender, amount); - emit Approval(owner, spender, amount); + emit IERC20Events.Approval(owner, spender, amount); return abi.encode(true); } return _redirectForHRC719(selector); @@ -844,7 +844,7 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { require(from != address(0), "hts: invalid sender"); require(to != address(0), "hts: invalid receiver"); _update(from, to, amount); - emit Transfer(from, to, amount); + emit IERC20Events.Transfer(from, to, amount); } function _transferNFT(address sender, address from, address to, uint256 serialId) private { @@ -872,7 +872,7 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { // Set the new owner assembly { sstore(slot, to) } - emit Transfer(from, to, serialId); + emit IERC721Events.Transfer(from, to, serialId); } function _update(address from, address to, uint256 amount) public { @@ -917,8 +917,7 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { bytes32 slot = _getApprovedSlot(uint32(serialId)); address newApproved = isApproved ? spender : address(0); assembly { sstore(slot, newApproved) } - - emit Approval(owner, spender, serialId); + emit IERC721Events.Approval(owner, spender, serialId); } /** @@ -939,6 +938,6 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { require(operator != address(0) && operator != sender, "setApprovalForAll: invalid operator"); bytes32 slot = _isApprovedForAllSlot(sender, operator); assembly { sstore(slot, approved) } - emit ApprovalForAll(sender, operator, approved); + emit IERC721Events.ApprovalForAll(sender, operator, approved); } } diff --git a/contracts/IERC20.sol b/contracts/IERC20.sol index 24baa9f3..bf75c95d 100644 --- a/contracts/IERC20.sol +++ b/contracts/IERC20.sol @@ -52,7 +52,7 @@ interface IERC20 { /** * @dev Returns the value of tokens owned by `account`. - */ + */ function balanceOf(address account) external view returns (uint256); /** diff --git a/contracts/IERC721.sol b/contracts/IERC721.sol index a7189e07..109acfc4 100644 --- a/contracts/IERC721.sol +++ b/contracts/IERC721.sol @@ -2,9 +2,34 @@ pragma solidity ^0.8.0; /** + * These events should be emitted by `transfer|transferFrom`, `approve` and `setApprovalForAll` respectively. + * * See https://ethereum.org/en/developers/docs/standards/tokens/erc-721/#events for more information. */ interface IERC721Events { + /** + * @dev Emitted when ownership of any NFT changes by any mechanism. + * This event emits when NFTs are created (`from` == 0) and destroyed (`to` == 0). + * Otherwise, it indicates that the token with ID {tokenId} was transferred from {from} to {to}, + * where {from} represents the previous owner of the token, not the approved spender. + * + * Exception: during contract creation, any number of NFTs may be created and assigned without emitting Transfer. + * + * At the time of any transfer, the approved address for that NFT (if any) is reset to none. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when the approved address for an NFT is changed or reaffirmed from {from} to {to} address. + * The zero {to} address indicates there will be no approved address. + * Additionally the approved address for that NFT (if any) is reset to none. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when an operator {operator} is enabled or disabled {approved} + * for an owner {owner}. The operator {operator} can than manage all NFTs of the owner {owner}. + */ event ApprovalForAll(address indexed owner, address indexed operator, bool approved); } @@ -45,6 +70,8 @@ interface IERC721 { * * Requirements: * - `serialId` must exist. + * + * This value changes when {approve} or {transferFrom} are called. */ function ownerOf(uint256 serialId) external view returns (address); diff --git a/test/ERC721.t.sol b/test/ERC721.t.sol index b5cac6b9..ede9d963 100644 --- a/test/ERC721.t.sol +++ b/test/ERC721.t.sol @@ -4,10 +4,9 @@ pragma solidity ^0.8.0; import {HtsSystemContract} from "../contracts/HtsSystemContract.sol"; import {Test, console} from "forge-std/Test.sol"; import {IERC721, IERC721Events} from "../contracts/IERC721.sol"; -import {IERC20Events} from "../contracts/IERC20.sol"; import {TestSetup} from "./lib/TestSetup.sol"; -contract ERC721TokenTest is Test, TestSetup, IERC721Events, IERC20Events { +contract ERC721TokenTest is Test, TestSetup, IERC721Events { function setUp() external { setUpMockStorageForNonFork(); diff --git a/test/HTS.t.sol b/test/HTS.t.sol index c71b6af7..12597d5c 100644 --- a/test/HTS.t.sol +++ b/test/HTS.t.sol @@ -686,7 +686,7 @@ contract HTSTest is Test, TestSetup { uint256 serialId = 1; vm.startPrank(CFNFTFF_TREASURY); vm.expectEmit(CFNFTFF); - emit IERC20Events.Transfer(CFNFTFF_TREASURY, to, serialId); + emit IERC721Events.Transfer(CFNFTFF_TREASURY, to, serialId); IHederaTokenService(HTS_ADDRESS).transferNFT(CFNFTFF, CFNFTFF_TREASURY, to, int64(int256(serialId))); vm.stopPrank(); assertEq(IERC721(CFNFTFF).ownerOf(serialId), to); @@ -697,7 +697,7 @@ contract HTSTest is Test, TestSetup { uint256 serialId = 1; vm.startPrank(CFNFTFF_TREASURY); vm.expectEmit(CFNFTFF); - emit IERC20Events.Transfer(CFNFTFF_TREASURY, to, serialId); + emit IERC721Events.Transfer(CFNFTFF_TREASURY, to, serialId); IHederaTokenService(HTS_ADDRESS).transferFromNFT(CFNFTFF, CFNFTFF_TREASURY, to, serialId); vm.stopPrank(); assertEq(IERC721(CFNFTFF).ownerOf(serialId), to); @@ -770,7 +770,7 @@ contract HTSTest is Test, TestSetup { to[0] = makeAddr("recipient"); vm.startPrank(CFNFTFF_TREASURY); vm.expectEmit(CFNFTFF); - emit IERC20Events.Transfer(CFNFTFF_TREASURY, to[0], serialId[0]); + emit IERC721Events.Transfer(CFNFTFF_TREASURY, to[0], serialId[0]); IHederaTokenService(HTS_ADDRESS).transferNFT(CFNFTFF, from[0], to[0], int64(int256(serialId[0]))); vm.stopPrank(); assertEq(IERC721(CFNFTFF).ownerOf(serialId[0]), to[0]); @@ -811,7 +811,7 @@ contract HTSTest is Test, TestSetup { assertNotEq(IERC721(token).getApproved(1), newSpender); vm.prank(CFNFTFF_TREASURY); vm.expectEmit(token); - emit IERC20Events.Approval(CFNFTFF_TREASURY, newSpender, 1); + emit IERC721Events.Approval(CFNFTFF_TREASURY, newSpender, 1); int64 responseCodeApprove = IHederaTokenService(HTS_ADDRESS).approveNFT(token, newSpender, 1); assertEq(responseCodeApprove, HederaResponseCodes.SUCCESS); assertEq(IERC721(token).getApproved(1), newSpender);