diff --git a/documentation/tutorial-examples/solidity/.gitignore b/documentation/tutorial-examples/solidity/.gitignore new file mode 100644 index 0000000000..27b83e2baf --- /dev/null +++ b/documentation/tutorial-examples/solidity/.gitignore @@ -0,0 +1,2 @@ +*.bin +*.abi \ No newline at end of file diff --git a/documentation/tutorial-examples/solidity/contracts/GetBalance.sol b/documentation/tutorial-examples/solidity/contracts/GetBalance.sol new file mode 100644 index 0000000000..46ee9ccaf3 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/GetBalance.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./core/ISC.sol"; + +contract GetBalance { + event GotAgentID(bytes agentID); + event GotBaseBalance(uint64 baseBalance); + event GotNativeTokenBalance(uint256 nativeTokenBalance); + event GotNFTIDs(uint256 nftBalance); + + function getBalance(bytes memory nativeTokenID) public returns(uint){ + ISCAgentID memory agentID = ISC.sandbox.getSenderAccount(); + emit GotAgentID(agentID.data); + + uint64 baseBalance = ISC.accounts.getL2BalanceBaseTokens(agentID); + emit GotBaseBalance(baseBalance); + + NativeTokenID memory id = NativeTokenID({ data: nativeTokenID}); + uint256 nativeTokens = ISC.accounts.getL2BalanceNativeTokens(id, agentID); + emit GotNativeTokenBalance(nativeTokens); + + uint256 nfts = ISC.accounts.getL2NFTAmount(agentID); + emit GotNativeTokenBalance(nfts); + + + return nativeTokens; + + } +} \ No newline at end of file diff --git a/documentation/tutorial-examples/solidity/contracts/core/ERC20BaseTokens.sol b/documentation/tutorial-examples/solidity/contracts/core/ERC20BaseTokens.sol new file mode 100644 index 0000000000..47aeff196a --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ERC20BaseTokens.sol @@ -0,0 +1,163 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; +import "./ISCSandbox.sol"; +import "./ISCPrivileged.sol"; +import "./ISCAccounts.sol"; + +/** + * @title ERC20BaseTokens + * @dev The ERC20 contract directly mapped to the L1 base token. + */ +contract ERC20BaseTokens { + uint256 private constant MAX_UINT64 = type(uint64).max; + + /** + * @dev Emitted when the approval of tokens is granted by a token owner to a spender. + * + * This event indicates that the token owner has approved the spender to transfer a certain amount of tokens on their behalf. + * + * @param tokenOwner The address of the token owner who granted the approval. + * @param spender The address of the spender who is granted the approval. + * @param tokens The amount of tokens approved for transfer. + */ + event Approval( + address indexed tokenOwner, + address indexed spender, + uint256 tokens + ); + + /** + * @dev Emitted when tokens are transferred from one address to another. + * + * This event indicates that a certain amount of tokens has been transferred from one address to another. + * + * @param from The address from which the tokens are transferred. + * @param to The address to which the tokens are transferred. + * @param tokens The amount of tokens transferred. + */ + event Transfer(address indexed from, address indexed to, uint256 tokens); + + /** + * @dev Returns the name of the base token. + * @return The name of the base token. + */ + function name() public view returns (string memory) { + return __iscSandbox.getBaseTokenProperties().name; + } + + /** + * @dev Returns the symbol of the base token. + * @return The symbol of the base token. + */ + function symbol() public view returns (string memory) { + return __iscSandbox.getBaseTokenProperties().tickerSymbol; + } + + /** + * @dev Returns the number of decimals used by the base token. + * @return The number of decimals used by the base token. + */ + function decimals() public view returns (uint8) { + return __iscSandbox.getBaseTokenProperties().decimals; + } + + /** + * @dev Returns the total supply of the base token. + * @return The total supply of the base token. + */ + function totalSupply() public view returns (uint256) { + return __iscSandbox.getBaseTokenProperties().totalSupply; + } + + /** + * @dev Returns the balance of the specified token owner. + * @param tokenOwner The address of the token owner. + * @return The balance of the token owner. + */ + function balanceOf(address tokenOwner) public view returns (uint256) { + ISCChainID chainID = __iscSandbox.getChainID(); + ISCAgentID memory ownerAgentID = ISCTypes.newEthereumAgentID( + tokenOwner, + chainID + ); + return __iscAccounts.getL2BalanceBaseTokens(ownerAgentID); + } + + /** + * @dev Transfers tokens from the caller's account to the specified receiver. + * @param receiver The address of the receiver. + * @param numTokens The number of tokens to transfer. + * @return true. + */ + function transfer( + address receiver, + uint256 numTokens + ) public returns (bool) { + require(numTokens <= MAX_UINT64, "amount is too large"); + ISCAssets memory assets; + assets.baseTokens = uint64(numTokens); + __iscPrivileged.moveBetweenAccounts(msg.sender, receiver, assets); + emit Transfer(msg.sender, receiver, numTokens); + return true; + } + + /** + * @dev Sets the allowance of `delegate` over the caller's tokens. + * @param delegate The address of the delegate. + * @param numTokens The number of tokens to allow. + * @return true. + */ + function approve( + address delegate, + uint256 numTokens + ) public returns (bool) { + __iscPrivileged.setAllowanceBaseTokens(msg.sender, delegate, numTokens); + emit Approval(msg.sender, delegate, numTokens); + return true; + } + + /** + * @dev Returns the allowance of the specified owner for the specified delegate. + * @param owner The address of the owner. + * @param delegate The address of the delegate. + * @return The allowance of the owner for the delegate. + */ + function allowance( + address owner, + address delegate + ) public view returns (uint256) { + ISCAssets memory assets = __iscSandbox.getAllowance(owner, delegate); + return assets.baseTokens; + } + + /** + * @dev Transfers tokens from the specified owner's account to the specified buyer. + * @param owner The address of the owner. + * @param buyer The address of the buyer. + * @param numTokens The number of tokens to transfer. + * @return true. + */ + function transferFrom( + address owner, + address buyer, + uint256 numTokens + ) public returns (bool) { + require(numTokens <= MAX_UINT64, "amount is too large"); + ISCAssets memory assets; + assets.baseTokens = uint64(numTokens); + __iscPrivileged.moveAllowedFunds(owner, msg.sender, assets); + if (buyer != msg.sender) { + __iscPrivileged.moveBetweenAccounts(msg.sender, buyer, assets); + } + emit Transfer(owner, buyer, numTokens); + return true; + } +} + +ERC20BaseTokens constant __erc20BaseTokens = ERC20BaseTokens( + ISC_ERC20BASETOKENS_ADDRESS +); diff --git a/documentation/tutorial-examples/solidity/contracts/core/ERC20ExternalNativeTokens.sol b/documentation/tutorial-examples/solidity/contracts/core/ERC20ExternalNativeTokens.sol new file mode 100644 index 0000000000..3292eabe98 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ERC20ExternalNativeTokens.sol @@ -0,0 +1,38 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ERC20NativeTokens.sol"; + +/** + * @title ERC20ExternalNativeTokens + * @dev The ERC20 contract for externally registered native tokens (off-chain foundry). + */ +contract ERC20ExternalNativeTokens is ERC20NativeTokens { + NativeTokenID private _nativeTokenID; + + // TODO: this value is set at contract creation, and may get outdated + uint256 private _maximumSupply; + + /** + * @dev Returns the native token ID. + * @return The native token ID. + */ + function nativeTokenID() + public + view + override + returns (NativeTokenID memory) + { + return _nativeTokenID; + } + + /** + * @dev Returns the total supply of the native tokens. + * @return The total supply of the native tokens. + */ + function totalSupply() public view override returns (uint256) { + return _maximumSupply; + } +} diff --git a/documentation/tutorial-examples/solidity/contracts/core/ERC20NativeTokens.sol b/documentation/tutorial-examples/solidity/contracts/core/ERC20NativeTokens.sol new file mode 100644 index 0000000000..aefe635473 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ERC20NativeTokens.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; +import "./ISCSandbox.sol"; +import "./ISCAccounts.sol"; +import "./ISCPrivileged.sol"; + +/** + * @title ERC20NativeTokens + * @dev The ERC20 contract native tokens (on-chain foundry). + */ +contract ERC20NativeTokens { + string private _name; + string private _tickerSymbol; + uint8 private _decimals; + + /** + * @dev Emitted when the allowance of a spender for an owner is set. + * @param tokenOwner The owner of the tokens. + * @param spender The address allowed to spend the tokens. + * @param tokens The amount of tokens allowed to be spent. + */ + event Approval( + address indexed tokenOwner, + address indexed spender, + uint256 tokens + ); + + /** + * @dev Emitted when tokens are transferred from one address to another. + * @param from The address tokens are transferred from. + * @param to The address tokens are transferred to. + * @param tokens The amount of tokens transferred. + */ + event Transfer(address indexed from, address indexed to, uint256 tokens); + + /** + * @dev Returns the foundry serial number of the native token. + * @return The foundry serial number. + */ + function foundrySerialNumber() internal view returns (uint32) { + return __iscSandbox.erc20NativeTokensFoundrySerialNumber(address(this)); + } + + /** + * @dev Returns the native token ID of the native token. + * @return The native token ID. + */ + function nativeTokenID() + public + view + virtual + returns (NativeTokenID memory) + { + return __iscSandbox.getNativeTokenID(foundrySerialNumber()); + } + + /** + * @dev Returns the name of the native token. + * @return The name of the token. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev Returns the ticker symbol of the native token. + * @return The ticker symbol of the token. + */ + function symbol() public view returns (string memory) { + return _tickerSymbol; + } + + /** + * @dev Returns the number of decimals used for the native token. + * @return The number of decimals. + */ + function decimals() public view returns (uint8) { + return _decimals; + } + + /** + * @dev Returns the total supply of the native token. + * @return The total supply of the token. + */ + function totalSupply() public view virtual returns (uint256) { + return + __iscSandbox + .getNativeTokenScheme(foundrySerialNumber()) + .maximumSupply; + } + + /** + * @dev Returns the balance of a token owner. + * @param tokenOwner The address of the token owner. + * @return The balance of the token owner. + */ + function balanceOf(address tokenOwner) public view returns (uint256) { + ISCChainID chainID = __iscSandbox.getChainID(); + ISCAgentID memory ownerAgentID = ISCTypes.newEthereumAgentID( + tokenOwner, + chainID + ); + return + __iscAccounts.getL2BalanceNativeTokens( + nativeTokenID(), + ownerAgentID + ); + } + + /** + * @dev Transfers tokens from the sender's address to the receiver's address. + * @param receiver The address to transfer tokens to. + * @param numTokens The amount of tokens to transfer. + * @return true. + */ + function transfer( + address receiver, + uint256 numTokens + ) public returns (bool) { + ISCAssets memory assets; + assets.nativeTokens = new NativeToken[](1); + assets.nativeTokens[0].ID = nativeTokenID(); + assets.nativeTokens[0].amount = numTokens; + __iscPrivileged.moveBetweenAccounts(msg.sender, receiver, assets); + emit Transfer(msg.sender, receiver, numTokens); + return true; + } + + /** + * @dev Sets the allowance of a spender to spend tokens on behalf of the owner. + * @param delegate The address allowed to spend the tokens. + * @param numTokens The amount of tokens allowed to be spent. + * @return true. + */ + function approve( + address delegate, + uint256 numTokens + ) public returns (bool) { + __iscPrivileged.setAllowanceNativeTokens( + msg.sender, + delegate, + nativeTokenID(), + numTokens + ); + emit Approval(msg.sender, delegate, numTokens); + return true; + } + + /** + * @dev Returns the amount of tokens that the spender is allowed to spend on behalf of the owner. + * @param owner The address of the token owner. + * @param delegate The address of the spender. + * @return The amount of tokens the spender is allowed to spend. + */ + function allowance( + address owner, + address delegate + ) public view returns (uint256) { + ISCAssets memory assets = __iscSandbox.getAllowance(owner, delegate); + NativeTokenID memory myID = nativeTokenID(); + for (uint256 i = 0; i < assets.nativeTokens.length; i++) { + if (bytesEqual(assets.nativeTokens[i].ID.data, myID.data)) + return assets.nativeTokens[i].amount; + } + return 0; + } + + /** + * @dev Compares two byte arrays for equality. + * @param a The first byte array. + * @param b The second byte array. + * @return A boolean indicating whether the byte arrays are equal or not. + */ + function bytesEqual( + bytes memory a, + bytes memory b + ) internal pure returns (bool) { + if (a.length != b.length) return false; + for (uint256 i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + + /** + * @dev Transfers tokens from one address to another on behalf of a token owner. + * @param owner The address from which tokens are transferred. + * @param buyer The address to which tokens are transferred. + * @param numTokens The amount of tokens to transfer. + * @return A boolean indicating whether the transfer was successful or not. + */ + function transferFrom( + address owner, + address buyer, + uint256 numTokens + ) public returns (bool) { + ISCAssets memory assets; + assets.nativeTokens = new NativeToken[](1); + assets.nativeTokens[0].ID = nativeTokenID(); + assets.nativeTokens[0].amount = numTokens; + __iscPrivileged.moveAllowedFunds(owner, msg.sender, assets); + if (buyer != msg.sender) { + __iscPrivileged.moveBetweenAccounts(msg.sender, buyer, assets); + } + emit Transfer(owner, buyer, numTokens); + return true; + } +} diff --git a/documentation/tutorial-examples/solidity/contracts/core/ERC721NFTCollection.sol b/documentation/tutorial-examples/solidity/contracts/core/ERC721NFTCollection.sol new file mode 100644 index 0000000000..021b194b9d --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ERC721NFTCollection.sol @@ -0,0 +1,63 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; +import "./ISCSandbox.sol"; +import "./ISCAccounts.sol"; +import "./ISCPrivileged.sol"; +import "./ERC721NFTs.sol"; + +/** + * @title ERC721NFTCollection + * @dev The ERC721 contract for a L2 collection of ISC NFTs, as defined in IRC27. + * Implements the ERC721 standard and extends the ERC721NFTs contract. + * For more information about IRC27, refer to: https://github.com/iotaledger/tips/blob/main/tips/TIP-0027/tip-0027.md + */ +contract ERC721NFTCollection is ERC721NFTs { + using ISCTypes for ISCNFT; + + NFTID private _collectionId; + string private _collectionName; // extracted from the IRC27 metadata + + /** + * @dev Returns the balance of the specified owner. + * @param owner The address to query the balance of. + * @return The balance of the specified owner. + */ + function _balanceOf( + ISCAgentID memory owner + ) internal view virtual override returns (uint256) { + return __iscAccounts.getL2NFTAmountInCollection(owner, _collectionId); + } + + /** + * @dev Checks if the given NFT is managed by this contract. + * @param nft The NFT to check. + * @return True if the NFT is managed by this contract, false otherwise. + */ + function _isManagedByThisContract( + ISCNFT memory nft + ) internal view virtual override returns (bool) { + return nft.isInCollection(_collectionId); + } + + /** + * @dev Returns the ID of the collection. + * @return The ID of the collection. + */ + function collectionId() external view virtual returns (NFTID) { + return _collectionId; + } + + // IERC721Metadata + + /** + * @dev Returns the name of the collection. + * @return The name of the collection. + */ + function name() external view virtual override returns (string memory) { + return _collectionName; + } +} diff --git a/documentation/tutorial-examples/solidity/contracts/core/ERC721NFTs.sol b/documentation/tutorial-examples/solidity/contracts/core/ERC721NFTs.sol new file mode 100644 index 0000000000..1ed55522a0 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ERC721NFTs.sol @@ -0,0 +1,319 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; +import "./ISCSandbox.sol"; +import "./ISCAccounts.sol"; +import "./ISCPrivileged.sol"; + +/** + * @title ERC721NFTs + * @dev This contract represents the ERC721 contract for the "global" collection of native NFTs on the chains L1 account. + */ +contract ERC721NFTs { + // is IERC721Metadata, IERC721, IERC165 + using ISCTypes for ISCAgentID; + using ISCTypes for uint256; + + // Mapping from token ID to approved address + mapping(uint256 => address) private _tokenApprovals; + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * @dev Emitted when a token is transferred from one address to another. + * + * @param from The address transferring the token. + * @param to The address receiving the token. + * @param tokenId The ID of the token being transferred. + */ + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + + /** + * @dev Emitted when the approval of a token is changed or reaffirmed. + * + * @param owner The owner of the token. + * @param approved The new approved address. + * @param tokenId The ID of the token. + */ + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + /** + * @dev Emitted when operator gets the allowance from owner. + * + * @param owner The owner of the token. + * @param operator The operator to get the approval. + * @param approved True if the operator got approval, false if not. + */ + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + + function _balanceOf( + ISCAgentID memory owner + ) internal view virtual returns (uint256) { + return __iscAccounts.getL2NFTAmount(owner); + } + + // virtual function meant to be overridden. ERC721NFTs manages all NFTs, regardless of + // whether they belong to any collection or not. + function _isManagedByThisContract( + ISCNFT memory + ) internal view virtual returns (bool) { + return true; + } + + /** + * @dev Returns the number of tokens owned by a specific address. + * @param owner The address to query the balance of. + * @return The balance of the specified address. + */ + function balanceOf(address owner) public view returns (uint256) { + ISCChainID chainID = __iscSandbox.getChainID(); + return _balanceOf(ISCTypes.newEthereumAgentID(owner, chainID)); + } + + /** + * @dev Returns the owner of the specified token ID. + * @param tokenId The ID of the token to query the owner for. + * @return The address of the owner of the token. + */ + function ownerOf(uint256 tokenId) public view returns (address) { + ISCNFT memory nft = __iscSandbox.getNFTData(tokenId.asNFTID()); + require(nft.owner.isEthereum()); + require(_isManagedByThisContract(nft)); + return nft.owner.ethAddress(); + } + + /** + * @dev Safely transfers an ERC721 token from one address to another. + * + * Emits a `Transfer` event. + * + * Requirements: + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - The token must exist and be owned by `from`. + * - If `to` is a smart contract, it must implement the `onERC721Received` function and return the magic value. + * + * @param from The address to transfer the token from. + * @param to The address to transfer the token to. + * @param tokenId The ID of the token to be transferred. + * @param data Additional data with no specified format, to be passed to the `onERC721Received` function if `to` is a smart contract. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public payable { + transferFrom(from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, data)); + } + + /** + * @dev Safely transfers an ERC721 token from one address to another. + * + * Emits a `Transfer` event. + * + * Requirements: + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - The caller must own the token or be approved for it. + * + * @param from The address to transfer the token from. + * @param to The address to transfer the token to. + * @param tokenId The ID of the token to be transferred. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public payable { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev Transfers an ERC721 token from one address to another. + * Emits a {Transfer} event. + * + * Requirements: + * - The caller must be approved or the owner of the token. + * + * @param from The address to transfer the token from. + * @param to The address to transfer the token to. + * @param tokenId The ID of the token to be transferred. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public payable { + require(_isApprovedOrOwner(msg.sender, tokenId)); + _transferFrom(from, to, tokenId); + } + + /** + * @dev Approves another address to transfer the ownership of a specific token. + * @param approved The address to be approved for token transfer. + * @param tokenId The ID of the token to be approved for transfer. + * @notice Only the owner of the token or an approved operator can call this function. + */ + function approve(address approved, uint256 tokenId) public payable { + address owner = ownerOf(tokenId); + require(approved != owner); + require(msg.sender == owner || isApprovedForAll(owner, msg.sender)); + + _tokenApprovals[tokenId] = approved; + emit Approval(owner, approved, tokenId); + } + + /** + * @dev Sets or revokes approval for the given operator to manage all of the caller's tokens. + * @param operator The address of the operator to set approval for. + * @param approved A boolean indicating whether to approve or revoke the operator's approval. + */ + function setApprovalForAll(address operator, bool approved) public { + require(operator != msg.sender); + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + /** + * @dev Returns the address that has been approved to transfer the ownership of the specified token. + * @param tokenId The ID of the token. + * @return The address approved to transfer the ownership of the token. + */ + function getApproved(uint256 tokenId) public view returns (address) { + return _tokenApprovals[tokenId]; + } + + /** + * @dev Checks if an operator is approved to manage all of the owner's tokens. + * @param owner The address of the token owner. + * @param operator The address of the operator. + * @return A boolean value indicating whether the operator is approved for all tokens of the owner. + */ + function isApprovedForAll( + address owner, + address operator + ) public view returns (bool) { + return _operatorApprovals[owner][operator]; + } + + function _isApprovedOrOwner( + address spender, + uint256 tokenId + ) internal view returns (bool) { + address owner = ownerOf(tokenId); + return (spender == owner || + getApproved(tokenId) == spender || + isApprovedForAll(owner, spender)); + } + + function _transferFrom(address from, address to, uint256 tokenId) internal { + require(ownerOf(tokenId) == from); + require(to != address(0)); + _clearApproval(tokenId); + + ISCAssets memory allowance; + allowance.nfts = new NFTID[](1); + allowance.nfts[0] = tokenId.asNFTID(); + + __iscPrivileged.moveBetweenAccounts(from, to, allowance); + + emit Transfer(from, to, tokenId); + } + + function _clearApproval(uint256 tokenId) private { + if (_tokenApprovals[tokenId] != address(0)) { + _tokenApprovals[tokenId] = address(0); + } + } + + // ERC165 + + bytes4 private constant _INTERFACE_ID_ERC721METADATA = 0x5b5e139f; + bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd; + bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; + + /** + * @dev Checks if a contract supports a given interface. + * @param interfaceID The interface identifier. + * @return A boolean value indicating whether the contract supports the interface. + */ + function supportsInterface(bytes4 interfaceID) public pure returns (bool) { + return + interfaceID == _INTERFACE_ID_ERC165 || + interfaceID == _INTERFACE_ID_ERC721 || + interfaceID == _INTERFACE_ID_ERC721METADATA; + } + + bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; + + function _checkOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory data + ) internal returns (bool) { + if (!_isContract(to)) { + return true; + } + + bytes4 retval = IERC721Receiver(to).onERC721Received( + msg.sender, + from, + tokenId, + data + ); + return (retval == _ERC721_RECEIVED); + } + + function _isContract(address account) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + // IERC721Metadata + + function name() external view virtual returns (string memory) { + return ""; + } + + function symbol() external pure returns (string memory) { + return ""; // not defined in IRC27 + } + + function tokenURI(uint256 tokenId) external view returns (string memory) { + IRC27NFT memory nft = __iscSandbox.getIRC27NFTData(tokenId.asNFTID()); + require(_isManagedByThisContract(nft.nft)); + return nft.metadata.uri; + } +} + +ERC721NFTs constant __erc721NFTs = ERC721NFTs(ISC_ERC721_ADDRESS); + +interface IERC721Receiver { + function onERC721Received( + address _operator, + address _from, + uint256 _tokenId, + bytes memory _data + ) external returns (bytes4); +} diff --git a/documentation/tutorial-examples/solidity/contracts/core/ISC.sol b/documentation/tutorial-examples/solidity/contracts/core/ISC.sol new file mode 100644 index 0000000000..dcc5404485 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ISC.sol @@ -0,0 +1,41 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCSandbox.sol"; +import "./ISCAccounts.sol"; +import "./ISCUtil.sol"; +import "./ISCPrivileged.sol"; +import "./ERC20BaseTokens.sol"; +import "./ERC20NativeTokens.sol"; +import "./ERC721NFTs.sol"; +import "./ERC721NFTCollection.sol"; + +/** + * @title ISC Library + * @dev This library contains various interfaces and functions related to the IOTA Smart Contracts (ISC) system. + * It provides access to the ISCSandbox, ISCAccounts, ISCUtil, ERC20BaseTokens, ERC20NativeTokens, ERC721NFTs, and ERC721NFTCollection contracts. + */ +library ISC { + ISCSandbox constant sandbox = __iscSandbox; + + ISCAccounts constant accounts = __iscAccounts; + + ISCUtil constant util = __iscUtil; + + ERC20BaseTokens constant baseTokens = __erc20BaseTokens; + + // Get the ERC20NativeTokens contract for the given foundry serial number + function nativeTokens(uint32 foundrySN) internal view returns (ERC20NativeTokens) { + return ERC20NativeTokens(sandbox.erc20NativeTokensAddress(foundrySN)); + } + + ERC721NFTs constant nfts = __erc721NFTs; + + // Get the ERC721NFTCollection contract for the given collection + function erc721NFTCollection(NFTID collectionID) internal view returns (ERC721NFTCollection) { + return ERC721NFTCollection(sandbox.erc721NFTCollectionAddress(collectionID)); + } + +} diff --git a/documentation/tutorial-examples/solidity/contracts/core/ISCAccounts.sol b/documentation/tutorial-examples/solidity/contracts/core/ISCAccounts.sol new file mode 100644 index 0000000000..c4319e36b7 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ISCAccounts.sol @@ -0,0 +1,41 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; + +/** + * @title ISCAccounts + * @dev Functions of the ISC Magic Contract to access the core accounts functionality + */ +interface ISCAccounts { + // Get the L2 base tokens balance of an account + function getL2BalanceBaseTokens(ISCAgentID memory agentID) external view returns (uint64); + + // Get the L2 native tokens balance of an account + function getL2BalanceNativeTokens(NativeTokenID memory id, ISCAgentID memory agentID) external view returns (uint256); + + // Get the L2 NFTs owned by an account + function getL2NFTs(ISCAgentID memory agentID) external view returns (NFTID[] memory); + + // Get the amount of L2 NFTs owned by an account + function getL2NFTAmount(ISCAgentID memory agentID) external view returns (uint256); + + // Get the L2 NFTs of a given collection owned by an account + function getL2NFTsInCollection(ISCAgentID memory agentID, NFTID collectionId) external view returns (NFTID[] memory); + + // Get the amount of L2 NFTs of a given collection owned by an account + function getL2NFTAmountInCollection(ISCAgentID memory agentID, NFTID collectionId) external view returns (uint256); + + // Create a new foundry. + function foundryCreateNew(NativeTokenScheme memory tokenScheme, ISCAssets memory allowance) external returns(uint32); + + // Creates foundry + IRC30 metadata + ERC20 token registration + function createNativeTokenFoundry(string memory tokenName, string memory tokenSymbol, uint8 tokenDecimals, NativeTokenScheme memory tokenScheme, ISCAssets memory allowance) external returns(uint32); + + // Mint new tokens. Only the owner of the foundry can call this function. + function mintNativeTokens(uint32 foundrySN, uint256 amount, ISCAssets memory allowance) external; +} + +ISCAccounts constant __iscAccounts = ISCAccounts(ISC_MAGIC_ADDRESS); diff --git a/documentation/tutorial-examples/solidity/contracts/core/ISCPrivileged.sol b/documentation/tutorial-examples/solidity/contracts/core/ISCPrivileged.sol new file mode 100644 index 0000000000..6924f5f278 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ISCPrivileged.sol @@ -0,0 +1,40 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; + +/** + * @title ISCPrivileged + * @dev The ISCPrivileged interface represents a contract that has some extra methods not included in the standard ISC interface. + * These methods can only be called from privileged contracts. + */ +interface ISCPrivileged { + function moveBetweenAccounts( + address sender, + address receiver, + ISCAssets memory allowance + ) external; + + function setAllowanceBaseTokens( + address from, + address to, + uint256 numTokens + ) external; + + function setAllowanceNativeTokens( + address from, + address to, + NativeTokenID memory nativeTokenID, + uint256 numTokens + ) external; + + function moveAllowedFunds( + address from, + address to, + ISCAssets memory allowance + ) external; +} + +ISCPrivileged constant __iscPrivileged = ISCPrivileged(ISC_MAGIC_ADDRESS); diff --git a/documentation/tutorial-examples/solidity/contracts/core/ISCSandbox.sol b/documentation/tutorial-examples/solidity/contracts/core/ISCSandbox.sol new file mode 100644 index 0000000000..5d69be90b3 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ISCSandbox.sol @@ -0,0 +1,142 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; + +/** + * @title ISCSandbox + * @dev This is the main interface of the ISC Magic Contract. + */ +interface ISCSandbox { + // Get the ISC request ID + function getRequestID() external view returns (ISCRequestID memory); + + // Get the AgentID of the sender of the ISC request + function getSenderAccount() external view returns (ISCAgentID memory); + + // Trigger an ISC event + function triggerEvent(string memory s) external; + + // Get a random 32-bit value based on the hash of the current ISC state transaction + function getEntropy() external view returns (bytes32); + + // Allow the `target` EVM contract to take some funds from the caller's L2 account + function allow(address target, ISCAssets memory allowance) external; + + // Take some funds from the given address, which must have authorized first with `allow`. + // If `allowance` is empty, all allowed funds are taken. + function takeAllowedFunds(address addr, ISCAssets memory allowance) + external; + + // Get the amount of funds currently allowed by the given address to the caller + function getAllowanceFrom(address addr) + external + view + returns (ISCAssets memory); + + // Get the amount of funds currently allowed by the caller to the given address + function getAllowanceTo(address target) + external + view + returns (ISCAssets memory); + + // Get the amount of funds currently allowed between the given addresses + function getAllowance(address from, address to) + external + view + returns (ISCAssets memory); + + // Send an on-ledger request (or a regular transaction to any L1 address). + // The specified `assets` are transferred from the caller's + // L2 account to the `evm` core contract's account. + // The sent request will have the `evm` core contract as sender. It will + // include the transferred `assets`. + // The specified `allowance` must not be greater than `assets`. + function send( + L1Address memory targetAddress, + ISCAssets memory assets, + bool adjustMinimumStorageDeposit, + ISCSendMetadata memory metadata, + ISCSendOptions memory sendOptions + ) external payable; + + // Call the entry point of an ISC contract on the same chain. + function call( + ISCHname contractHname, + ISCHname entryPoint, + ISCDict memory params, + ISCAssets memory allowance + ) external returns (ISCDict memory); + + // Call a view entry point of an ISC contract on the same chain. + // The called entry point will have the `evm` core contract as caller. + function callView( + ISCHname contractHname, + ISCHname entryPoint, + ISCDict memory params + ) external view returns (ISCDict memory); + + // Get the ChainID of the underlying ISC chain + function getChainID() external view returns (ISCChainID); + + // Get the ISC chain's owner + function getChainOwnerID() external view returns (ISCAgentID memory); + + // Get the timestamp of the ISC block (seconds since UNIX epoch) + function getTimestampUnixSeconds() external view returns (int64); + + // Get the properties of the ISC base token + function getBaseTokenProperties() + external + view + returns (ISCTokenProperties memory); + + // Get the ID of a L2-controlled native token, given its foundry serial number + function getNativeTokenID(uint32 foundrySN) + external + view + returns (NativeTokenID memory); + + // Get the token scheme of a L2-controlled native token, given its foundry serial number + function getNativeTokenScheme(uint32 foundrySN) + external + view + returns (NativeTokenScheme memory); + + // Get information about an on-chain NFT + function getNFTData(NFTID id) external view returns (ISCNFT memory); + + // Get information about an on-chain IRC27 NFT + // Note: the metadata.uri field is encoded as a data URL with: + // base64(jsonEncode({ + // "name": NFT.name, + // "description": NFT.description, + // "image": NFT.URI + // })) + function getIRC27NFTData(NFTID id) external view returns (IRC27NFT memory); + + // Get the address of an ERC20NativeTokens contract for the given foundry serial number + function erc20NativeTokensAddress(uint32 foundrySN) + external + view + returns (address); + + // Get the address of an ERC721NFTCollection contract for the given collection ID + function erc721NFTCollectionAddress(NFTID collectionID) + external + view + returns (address); + + // Extract the foundry serial number from an ERC20NativeTokens contract's address + function erc20NativeTokensFoundrySerialNumber(address addr) + external + view + returns (uint32); + + // Creates an ERC20NativeTokens contract instance and register it with the foundry as a native token. Only the foundry owner can call this function. + function registerERC20NativeToken(uint32 foundrySN, string memory name, string memory symbol, uint8 decimals, ISCAssets memory allowance) external; +} + +ISCSandbox constant __iscSandbox = ISCSandbox(ISC_MAGIC_ADDRESS); diff --git a/documentation/tutorial-examples/solidity/contracts/core/ISCTypes.sol b/documentation/tutorial-examples/solidity/contracts/core/ISCTypes.sol new file mode 100644 index 0000000000..afaf907004 --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ISCTypes.sol @@ -0,0 +1,217 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +/** + * @dev Collection of types and constants used in the ISC system + */ + +// Every ISC chain is initialized with an instance of the Magic contract at this address +address constant ISC_MAGIC_ADDRESS = 0x1074000000000000000000000000000000000000; + +// The ERC20 contract for base tokens is at this address: +address constant ISC_ERC20BASETOKENS_ADDRESS = 0x1074010000000000000000000000000000000000; + +// The ERC721 contract for NFTs is at this address: +address constant ISC_ERC721_ADDRESS = 0x1074030000000000000000000000000000000000; + +// An L1 IOTA address +struct L1Address { + bytes data; +} + +uint8 constant L1AddressTypeEd25519 = 0; +uint8 constant L1AddressTypeAlias = 8; +uint8 constant L1AddressTypeNFT = 16; + +// An IOTA native token ID +struct NativeTokenID { + bytes data; +} + +// An amount of some IOTA native token +struct NativeToken { + NativeTokenID ID; + uint256 amount; +} + +// The scheme used to create an IOTA native token (corresponds to iotago.SimpleTokenScheme) +struct NativeTokenScheme { + uint256 mintedTokens; + uint256 meltedTokens; + uint256 maximumSupply; +} + +// An IOTA NFT ID +type NFTID is bytes32; + +// Information about an on-chain NFT +struct ISCNFT { + NFTID ID; + L1Address issuer; + bytes metadata; + ISCAgentID owner; +} + +struct IRC27NFTMetadata { + string standard; + string version; + string mimeType; + // Note: uri field is encoded as a data URL with: + // base64(jsonEncode({ + // "name": NFT.name, + // "description": NFT.description, + // "image": NFT.URI + // })) + string uri; + string name; +} + +// Information about an on-chain IRC27 NFT +struct IRC27NFT { + ISCNFT nft; + IRC27NFTMetadata metadata; +} + +// An ISC transaction ID +type ISCTransactionID is bytes32; + +// An ISC hname +type ISCHname is uint32; + +// An ISC chain ID +type ISCChainID is bytes32; + +// An ISC AgentID +struct ISCAgentID { + bytes data; +} + +uint8 constant ISCAgentIDKindNil = 0; +uint8 constant ISCAgentIDKindAddress = 1; +uint8 constant ISCAgentIDKindContract = 2; +uint8 constant ISCAgentIDKindEthereumAddress = 3; + +// An ISC request ID +struct ISCRequestID { + bytes data; +} + +// A single key-value pair +struct ISCDictItem { + bytes key; + bytes value; +} + +// Wrapper for the isc.Dict type, a collection of key-value pairs +struct ISCDict { + ISCDictItem[] items; +} + +// Parameters for building an on-ledger request +struct ISCSendMetadata { + ISCHname targetContract; + ISCHname entrypoint; + ISCDict params; + ISCAssets allowance; + uint64 gasBudget; +} + +// The specifies an amount of funds (tokens) for an ISC call. +struct ISCAssets { + uint64 baseTokens; + NativeToken[] nativeTokens; + NFTID[] nfts; +} + +// Parameters for building an on-ledger request +struct ISCSendOptions { + int64 timelock; + ISCExpiration expiration; +} + +// Expiration of an on-ledger request +struct ISCExpiration { + int64 time; + L1Address returnAddress; +} + +// Properties of an ISC base/native token +struct ISCTokenProperties { + string name; + string tickerSymbol; + uint8 decimals; + uint256 totalSupply; +} + +library ISCTypes { + function L1AddressType( + L1Address memory addr + ) internal pure returns (uint8) { + return uint8(addr.data[0]); + } + + function newEthereumAgentID( + address addr, + ISCChainID iscChainID + ) internal pure returns (ISCAgentID memory) { + bytes memory chainIDBytes = abi.encodePacked(iscChainID); + bytes memory addrBytes = abi.encodePacked(addr); + ISCAgentID memory r; + r.data = new bytes(1 + addrBytes.length + chainIDBytes.length); + r.data[0] = bytes1(ISCAgentIDKindEthereumAddress); + + //write chainID + for (uint i = 0; i < chainIDBytes.length; i++) { + r.data[i + 1] = chainIDBytes[i]; + } + + //write eth addr + for (uint i = 0; i < addrBytes.length; i++) { + r.data[i + 1 + chainIDBytes.length] = addrBytes[i]; + } + return r; + } + + function isEthereum(ISCAgentID memory a) internal pure returns (bool) { + return uint8(a.data[0]) == ISCAgentIDKindEthereumAddress; + } + + function ethAddress(ISCAgentID memory a) internal pure returns (address) { + bytes memory b = new bytes(20); + //offset of 33 (kind byte + chainID) + for (uint i = 0; i < 20; i++) b[i] = a.data[i + 33]; + return address(uint160(bytes20(b))); + } + + function chainID(ISCAgentID memory a) internal pure returns (ISCChainID) { + bytes32 out; + for (uint i = 0; i < 32; i++) { + //offset of 1 (kind byte) + out |= bytes32(a.data[1 + i] & 0xFF) >> (i * 8); + } + return ISCChainID.wrap(out); + } + + function asNFTID(uint256 tokenID) internal pure returns (NFTID) { + return NFTID.wrap(bytes32(tokenID)); + } + + function isInCollection( + ISCNFT memory nft, + NFTID collectionId + ) internal pure returns (bool) { + if (L1AddressType(nft.issuer) != L1AddressTypeNFT) { + return false; + } + assert(nft.issuer.data.length == 33); + bytes memory collectionIdBytes = abi.encodePacked(collectionId); + for (uint i = 0; i < 32; i++) { + if (collectionIdBytes[i] != nft.issuer.data[i + 1]) { + return false; + } + } + return true; + } +} diff --git a/documentation/tutorial-examples/solidity/contracts/core/ISCUtil.sol b/documentation/tutorial-examples/solidity/contracts/core/ISCUtil.sol new file mode 100644 index 0000000000..56b764fbaf --- /dev/null +++ b/documentation/tutorial-examples/solidity/contracts/core/ISCUtil.sol @@ -0,0 +1,20 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.11; + +import "./ISCTypes.sol"; + +/** + * @title ISCUtil + * @dev Functions of the ISC Magic Contract not directly related to the ISC sandbox + */ +interface ISCUtil { + // Get an ISC contract's hname given its instance name + function hn(string memory s) external pure returns (ISCHname); + + // Print something to the console (will only work when debugging contracts with Solo) + function print(string memory s) external pure; +} + +ISCUtil constant __iscUtil = ISCUtil(ISC_MAGIC_ADDRESS); diff --git a/documentation/tutorial-examples/solidity/tests/get_balance_test.go b/documentation/tutorial-examples/solidity/tests/get_balance_test.go new file mode 100644 index 0000000000..017261d95f --- /dev/null +++ b/documentation/tutorial-examples/solidity/tests/get_balance_test.go @@ -0,0 +1,75 @@ +package solidity_test + +import ( + _ "embed" + "math/big" + "slices" + "strings" + "testing" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/iotaledger/wasp/packages/solo" + // import utils + //"wasp/documentation/tutorial-examples/solidity/utils" +) + +//go:generate solc --abi --bin --overwrite ../contracts/GetBalance.sol -o build/ +var ( + //go:embed build/GetBalance.abi + GetBalanceContractABI string + //go:embed build/GetBalance.bin + GetBalanceContractBytecodeHex string + GetBalanceContractBytecode = common.FromHex(strings.TrimSpace(GetBalanceContractBytecodeHex)) +) + +func TestGetBalance(t *testing.T) { + env := solo.New(t, &solo.InitOptions{AutoAdjustStorageDeposit: true, Debug: true}) + chain := env.NewChain() + + private_key, user_address := chain.NewEthereumAccountWithL2Funds() + + contract_addr, abi := chain.DeployEVMContract(private_key, GetBalanceContractABI, GetBalanceContractBytecode, &big.Int{}) + + _, token_id, _ := chain.NewNativeTokenParams(1000000000).CreateFoundry() + + callArgs, _ := abi.Pack("getBalance", []byte(token_id.ToHex())) + + signer, _ := chain.EVM().Signer() + gas := uint64(1000000) + + callMsg := ethereum.CallMsg{ + From: user_address, + To: &contract_addr, + Data: callArgs, + Value: big.NewInt(0), + GasPrice: chain.EVM().GasPrice(), + } + + transaction, _ := types.SignNewTx(private_key, signer, &types.LegacyTx{ + Nonce: 1, + Gas: gas, + GasPrice: callMsg.GasPrice, + To: callMsg.To, + Value: big.NewInt(0), + Data: callMsg.Data, + }) + + chain.EVM().SendTransaction(transaction) + + receipt := chain.EVM().TransactionReceipt(transaction.Hash()) + + topic := abi.Events["GotBaseBalance"].ID + + var base_balance uint64 + for _, log := range receipt.Logs { + if slices.Contains(log.Topics, topic) { + err := abi.UnpackIntoInterface(base_balance, "GotBaseBalance", log.Data) + t.Log(err) + } + } + + t.Log("Value: ", base_balance) + +}