@paimaexample/evm-contracts 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +7 -0
  2. package/deno.json +28 -0
  3. package/docs/templates/contract.hbs +144 -0
  4. package/docs/templates/helpers.js +61 -0
  5. package/docs/templates/page.hbs +7 -0
  6. package/docs/templates/properties.js +76 -0
  7. package/hardhat.config.ts +11 -0
  8. package/index.js +1 -0
  9. package/mod.ts +0 -0
  10. package/package.json +13 -0
  11. package/remappings.txt +1 -0
  12. package/src/companions/ERC165Contract.json +21 -0
  13. package/src/companions/ERC165Contract.ts +21 -0
  14. package/src/companions/ERC20Contract.json +222 -0
  15. package/src/companions/ERC20Contract.ts +222 -0
  16. package/src/companions/ERC6551RegistryContract.json +128 -0
  17. package/src/companions/ERC6551RegistryContract.ts +128 -0
  18. package/src/companions/ERC721Contract.json +248 -0
  19. package/src/companions/ERC721Contract.ts +222 -0
  20. package/src/companions/IERC1155Contract.json +295 -0
  21. package/src/companions/IERC1155Contract.ts +295 -0
  22. package/src/companions/OldERC6551RegistryContract.json +133 -0
  23. package/src/companions/OldERC6551RegistryContract.ts +133 -0
  24. package/src/companions/PaimaERC721Contract.json +787 -0
  25. package/src/companions/PaimaERC721Contract.ts +787 -0
  26. package/src/companions/PaimaL2Contract.json +134 -0
  27. package/src/companions/PaimaL2Contract.ts +134 -0
  28. package/src/companions/README.md +5 -0
  29. package/src/contracts/AnnotatedMintNft.sol +171 -0
  30. package/src/contracts/BaseState.sol +16 -0
  31. package/src/contracts/ERC1967.sol +43 -0
  32. package/src/contracts/Erc20NftSale.sol +186 -0
  33. package/src/contracts/GenericPayment.sol +60 -0
  34. package/src/contracts/NativeNftSale.sol +97 -0
  35. package/src/contracts/PaimaL2Contract.sol +54 -0
  36. package/src/contracts/Proxy/Erc20NftSaleProxy.sol +79 -0
  37. package/src/contracts/Proxy/GenericPaymentProxy.sol +64 -0
  38. package/src/contracts/Proxy/NativeNftSaleProxy.sol +72 -0
  39. package/src/contracts/Proxy/OrderbookDexProxy.sol +27 -0
  40. package/src/contracts/README.md +72 -0
  41. package/src/contracts/State.sol +25 -0
  42. package/src/contracts/dev/ERC721Dev.sol +13 -0
  43. package/src/contracts/dev/Erc20Dev.sol +13 -0
  44. package/src/contracts/dev/NativeNftSaleUpgradeDev.sol +9 -0
  45. package/src/contracts/dev/NftSaleUpgradeDev.sol +12 -0
  46. package/src/contracts/dev/NftTypeMapper.sol +38 -0
  47. package/src/contracts/dev/Token.sol +15 -0
  48. package/src/contracts/dev/UpgradeDev.sol +10 -0
  49. package/src/contracts/orderbook/IOrderbookDex.sol +215 -0
  50. package/src/contracts/orderbook/OrderbookDex.sol +435 -0
  51. package/src/contracts/token/IERC4906Agnostic.sol +17 -0
  52. package/src/contracts/token/IInverseAppProjected1155.sol +40 -0
  53. package/src/contracts/token/IInverseAppProjectedNft.sol +38 -0
  54. package/src/contracts/token/IInverseBaseProjected1155.sol +25 -0
  55. package/src/contracts/token/IInverseBaseProjectedNft.sol +29 -0
  56. package/src/contracts/token/IInverseProjected1155.sol +38 -0
  57. package/src/contracts/token/IInverseProjectedNft.sol +41 -0
  58. package/src/contracts/token/ITokenUri.sol +10 -0
  59. package/src/contracts/token/IUri.sol +13 -0
  60. package/src/contracts/token/InverseAppProjected1155.sol +218 -0
  61. package/src/contracts/token/InverseAppProjectedNft.sol +192 -0
  62. package/src/contracts/token/InverseBaseProjected1155.sol +170 -0
  63. package/src/contracts/token/InverseBaseProjectedNft.sol +158 -0
  64. package/src/plugin/common.ts +35 -0
  65. package/src/plugin/deployment.ts +161 -0
  66. package/src/plugin/mod.ts +6 -0
  67. package/src/plugin/paimaL2.ts +202 -0
  68. package/src/recommendedHardhat.ts +86 -0
  69. package/test/lib/StdInvariant.sol +96 -0
  70. package/test/lib/cheatcodes.sol +89 -0
  71. package/test/lib/console.sol +1884 -0
  72. package/test/lib/ctest.sol +678 -0
  73. package/test/src/InverseAppProjected1155.t.sol +207 -0
  74. package/test/src/InverseAppProjectedNft.t.sol +164 -0
  75. package/test/src/InverseBaseProjected1155.t.sol +171 -0
  76. package/test/src/InverseBaseProjectedNft.t.sol +141 -0
  77. package/test/src/OrderbookDex.t.sol +710 -0
  78. package/test/src/OrderbookDexInvariant.t.sol +426 -0
  79. package/test/src/PaimaL2ContractTest.sol +115 -0
@@ -0,0 +1,134 @@
1
+ [
2
+ {
3
+ "inputs": [
4
+ {
5
+ "internalType": "address",
6
+ "name": "_owner",
7
+ "type": "address"
8
+ },
9
+ {
10
+ "internalType": "uint256",
11
+ "name": "_fee",
12
+ "type": "uint256"
13
+ }
14
+ ],
15
+ "stateMutability": "nonpayable",
16
+ "type": "constructor"
17
+ },
18
+ {
19
+ "anonymous": false,
20
+ "inputs": [
21
+ {
22
+ "indexed": true,
23
+ "internalType": "address",
24
+ "name": "userAddress",
25
+ "type": "address"
26
+ },
27
+ {
28
+ "indexed": false,
29
+ "internalType": "bytes",
30
+ "name": "data",
31
+ "type": "bytes"
32
+ },
33
+ {
34
+ "indexed": false,
35
+ "internalType": "uint256",
36
+ "name": "value",
37
+ "type": "uint256"
38
+ }
39
+ ],
40
+ "name": "PaimaGameInteraction",
41
+ "type": "event"
42
+ },
43
+ {
44
+ "inputs": [],
45
+ "name": "fee",
46
+ "outputs": [
47
+ {
48
+ "internalType": "uint256",
49
+ "name": "",
50
+ "type": "uint256"
51
+ }
52
+ ],
53
+ "stateMutability": "view",
54
+ "type": "function"
55
+ },
56
+ {
57
+ "inputs": [
58
+ {
59
+ "internalType": "address",
60
+ "name": "",
61
+ "type": "address"
62
+ }
63
+ ],
64
+ "name": "latestStoreHeight",
65
+ "outputs": [
66
+ {
67
+ "internalType": "uint256",
68
+ "name": "",
69
+ "type": "uint256"
70
+ }
71
+ ],
72
+ "stateMutability": "view",
73
+ "type": "function"
74
+ },
75
+ {
76
+ "inputs": [],
77
+ "name": "owner",
78
+ "outputs": [
79
+ {
80
+ "internalType": "address",
81
+ "name": "",
82
+ "type": "address"
83
+ }
84
+ ],
85
+ "stateMutability": "view",
86
+ "type": "function"
87
+ },
88
+ {
89
+ "inputs": [
90
+ {
91
+ "internalType": "bytes",
92
+ "name": "data",
93
+ "type": "bytes"
94
+ }
95
+ ],
96
+ "name": "paimaSubmitGameInput",
97
+ "outputs": [],
98
+ "stateMutability": "payable",
99
+ "type": "function"
100
+ },
101
+ {
102
+ "inputs": [],
103
+ "name": "withdrawFunds",
104
+ "outputs": [],
105
+ "stateMutability": "nonpayable",
106
+ "type": "function"
107
+ },
108
+ {
109
+ "inputs": [
110
+ {
111
+ "internalType": "address",
112
+ "name": "newOwner",
113
+ "type": "address"
114
+ }
115
+ ],
116
+ "name": "setOwner",
117
+ "outputs": [],
118
+ "stateMutability": "nonpayable",
119
+ "type": "function"
120
+ },
121
+ {
122
+ "inputs": [
123
+ {
124
+ "internalType": "uint256",
125
+ "name": "newFee",
126
+ "type": "uint256"
127
+ }
128
+ ],
129
+ "name": "setFee",
130
+ "outputs": [],
131
+ "stateMutability": "nonpayable",
132
+ "type": "function"
133
+ }
134
+ ]
@@ -0,0 +1,134 @@
1
+ export default [
2
+ {
3
+ inputs: [
4
+ {
5
+ internalType: "address",
6
+ name: "_owner",
7
+ type: "address",
8
+ },
9
+ {
10
+ internalType: "uint256",
11
+ name: "_fee",
12
+ type: "uint256",
13
+ },
14
+ ],
15
+ stateMutability: "nonpayable",
16
+ type: "constructor",
17
+ },
18
+ {
19
+ anonymous: false,
20
+ inputs: [
21
+ {
22
+ indexed: true,
23
+ internalType: "address",
24
+ name: "userAddress",
25
+ type: "address",
26
+ },
27
+ {
28
+ indexed: false,
29
+ internalType: "bytes",
30
+ name: "data",
31
+ type: "bytes",
32
+ },
33
+ {
34
+ indexed: false,
35
+ internalType: "uint256",
36
+ name: "value",
37
+ type: "uint256",
38
+ },
39
+ ],
40
+ name: "PaimaGameInteraction",
41
+ type: "event",
42
+ },
43
+ {
44
+ inputs: [],
45
+ name: "fee",
46
+ outputs: [
47
+ {
48
+ internalType: "uint256",
49
+ name: "",
50
+ type: "uint256",
51
+ },
52
+ ],
53
+ stateMutability: "view",
54
+ type: "function",
55
+ },
56
+ {
57
+ inputs: [
58
+ {
59
+ internalType: "address",
60
+ name: "",
61
+ type: "address",
62
+ },
63
+ ],
64
+ name: "latestStoreHeight",
65
+ outputs: [
66
+ {
67
+ internalType: "uint256",
68
+ name: "",
69
+ type: "uint256",
70
+ },
71
+ ],
72
+ stateMutability: "view",
73
+ type: "function",
74
+ },
75
+ {
76
+ inputs: [],
77
+ name: "owner",
78
+ outputs: [
79
+ {
80
+ internalType: "address",
81
+ name: "",
82
+ type: "address",
83
+ },
84
+ ],
85
+ stateMutability: "view",
86
+ type: "function",
87
+ },
88
+ {
89
+ inputs: [
90
+ {
91
+ internalType: "bytes",
92
+ name: "data",
93
+ type: "bytes",
94
+ },
95
+ ],
96
+ name: "paimaSubmitGameInput",
97
+ outputs: [],
98
+ stateMutability: "payable",
99
+ type: "function",
100
+ },
101
+ {
102
+ inputs: [],
103
+ name: "withdrawFunds",
104
+ outputs: [],
105
+ stateMutability: "nonpayable",
106
+ type: "function",
107
+ },
108
+ {
109
+ inputs: [
110
+ {
111
+ internalType: "address",
112
+ name: "newOwner",
113
+ type: "address",
114
+ },
115
+ ],
116
+ name: "setOwner",
117
+ outputs: [],
118
+ stateMutability: "nonpayable",
119
+ type: "function",
120
+ },
121
+ {
122
+ inputs: [
123
+ {
124
+ internalType: "uint256",
125
+ name: "newFee",
126
+ type: "uint256",
127
+ },
128
+ ],
129
+ name: "setFee",
130
+ outputs: [],
131
+ stateMutability: "nonpayable",
132
+ type: "function",
133
+ },
134
+ ] as const;
@@ -0,0 +1,5 @@
1
+ # Companion ABIs
2
+
3
+ ABIs for contracts that are not written by the Paima team, but whose ABIs are
4
+ useful either in conjunction to our contracts, or who are used by the Paima
5
+ Engine itself.
@@ -0,0 +1,171 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ pragma solidity ^0.8.20;
4
+
5
+ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
6
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
+ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
9
+
10
+ /// @dev A standard ERC721 that accepts calldata in the mint function for any initialization data needed in a Paima dApp.
11
+ /// Upon deployment only the contract owner (specified in constructor parameter) is able to mint tokens. Additional
12
+ /// minters (e.g. NativeNftSale contract) need to be added by using the `setMinter` function.
13
+ contract AnnotatedMintNft is ERC165, ERC721, Ownable {
14
+ /// @dev The token ID that will be minted when calling the `mint` function.
15
+ uint256 public currentTokenId;
16
+ /// @dev Base URI that is used in the `tokenURI` function to form the start of the token URI.
17
+ string public baseURI;
18
+ /// @dev Total token supply, increased by minting and decreased by burning.
19
+ uint256 public totalSupply;
20
+ /// @dev Maximum amount of tokens that is allowed to exist.
21
+ uint256 public maxSupply;
22
+ /// @dev Base extension that is used in the `tokenURI` function to form the end of the token URI.
23
+ string public baseExtension;
24
+
25
+ /// @dev Returns true for addresses that are allowed to mint this token.
26
+ mapping(address => bool) public minters;
27
+
28
+ /// @dev Reverts if `msg.sender` is neither a minter nor the contract owner.
29
+ modifier canMint() {
30
+ require(
31
+ isMinter(msg.sender) || owner() == msg.sender,
32
+ "AnnotatedMintNft: not authorized to mint"
33
+ );
34
+ _;
35
+ }
36
+
37
+ /// @dev Reverts if `msg.sender` is not the specified token's owner.
38
+ modifier onlyTokenOwner(uint256 tokenId) {
39
+ require(msg.sender == ownerOf(tokenId), "AnnotatedMintNft: not owner");
40
+ _;
41
+ }
42
+
43
+ /// @dev Emitted when max supply is updated from `oldMaxSupply` to `newMaxSupply`.
44
+ event UpdateMaxSupply(uint256 indexed oldMaxSupply, uint256 indexed newMaxSupply);
45
+
46
+ /// @dev Emitted when `newMinter` is added to the mapping of allowed `minters`.
47
+ event SetMinter(address indexed newMinter);
48
+
49
+ /// @dev Emitted when `oldMinter` is removed from the mapping of allowed `minters`
50
+ event RemoveMinter(address indexed oldMinter);
51
+
52
+ /// @dev Emitted when `baseUri` is updated from `oldUri` to `newUri`.
53
+ event SetBaseURI(string oldUri, string newUri);
54
+
55
+ /// @dev Emitted when a new token with ID `tokenId` is minted, with `initialData` provided in the `mint` function parameters.
56
+ event Minted(uint256 indexed tokenId, string initialData);
57
+
58
+ /// @dev Sets the NFT's `name`, `symbol`, `maxSupply` to `supply`, `currentTokenId` to 1 and `baseExtension` to `".json"`.
59
+ /// @param name Collection name.
60
+ /// @param symbol Collection symbol.
61
+ /// @param owner The owner of the contract, who will be able to execute
62
+ constructor(
63
+ string memory name,
64
+ string memory symbol,
65
+ uint256 supply,
66
+ address owner
67
+ ) ERC721(name, symbol) Ownable(owner) {
68
+ maxSupply = supply;
69
+ currentTokenId = 1;
70
+ baseExtension = ".json";
71
+ }
72
+
73
+ /// @dev Returns true if this contract implements the interface defined by `interfaceID`. See EIP165.
74
+ function supportsInterface(
75
+ bytes4 interfaceID
76
+ ) public view override(ERC165, ERC721) returns (bool) {
77
+ return super.supportsInterface(interfaceID) || interfaceID == this.mint.selector;
78
+ }
79
+
80
+ /// @dev Mints a new token to address `_to`, passing `initialData` to be emitted in the event.
81
+ /// Increases the `totalSupply` and `currentTokenId`.
82
+ /// Reverts if `totalSupply` is not less than `maxSupply` or if `_to` is a zero address.
83
+ /// Emits the `Minted` event.
84
+ function mint(address _to, string calldata initialData) external canMint returns (uint256) {
85
+ require(maxSupply > totalSupply, "AnnotatedMintNft: max supply reached");
86
+ require(_to != address(0), "AnnotatedMintNft: zero receiver address");
87
+
88
+ uint256 tokenId = currentTokenId;
89
+ _safeMint(_to, tokenId);
90
+
91
+ totalSupply++;
92
+ currentTokenId++;
93
+
94
+ emit Minted(tokenId, initialData);
95
+ return tokenId;
96
+ }
97
+
98
+ /// @dev Burns token of ID `_tokenId`. Callable only by the owner of the specified token.
99
+ /// Reverts if `_tokenId` is not existing.
100
+ function burn(uint256 _tokenId) external onlyTokenOwner(_tokenId) {
101
+ totalSupply--;
102
+ _burn(_tokenId);
103
+ }
104
+
105
+ /// @dev Adds `_minter` to the mapping of allowed `minters` of this NFT.
106
+ /// Callable only by the contract owner.
107
+ /// Emits the `SetMinter` event.
108
+ function setMinter(address _minter) external onlyOwner {
109
+ require(_minter != address(0), "AnnotatedMintNft: invalid minter");
110
+
111
+ minters[_minter] = true;
112
+ emit SetMinter(_minter);
113
+ }
114
+
115
+ /// @dev Removes `_minter` from the mapping of allowed `minters` of this NFT.
116
+ /// Callable only by the contract owner.
117
+ /// Emits the `RemoveMinter` event.
118
+ function removeMinter(address _minter) external onlyOwner {
119
+ require(_minter != address(0), "AnnotatedMintNft: invalid minter");
120
+
121
+ minters[_minter] = false;
122
+ emit RemoveMinter(_minter);
123
+ }
124
+
125
+ /// @dev Returns the `baseURI` of this NFT.
126
+ function _baseURI() internal view override returns (string memory) {
127
+ return baseURI;
128
+ }
129
+
130
+ /// @dev Returns the token URI of specified `tokenId`.
131
+ function tokenURI(uint256 tokenId) public view override returns (string memory) {
132
+ string memory URI = super.tokenURI(tokenId);
133
+ return string(abi.encodePacked(URI, baseExtension));
134
+ }
135
+
136
+ /// @dev Sets `_URI` as the `baseURI` of the NFT.
137
+ /// Callable only by the contract owner.
138
+ /// Emits the `SetBaseURI` event.
139
+ function setBaseURI(string memory _URI) external onlyOwner {
140
+ string memory oldURI = baseURI;
141
+ baseURI = _URI;
142
+ emit SetBaseURI(oldURI, _URI);
143
+ }
144
+
145
+ /// @dev Sets `_newBaseExtension` as the `baseExtension` of the NFT.
146
+ /// Callable only by the contract owner.
147
+ function setBaseExtension(string memory _newBaseExtension) public onlyOwner {
148
+ baseExtension = _newBaseExtension;
149
+ }
150
+
151
+ /// @dev Sets `_maxSupply` as the `maxSupply` of the NFT.
152
+ /// Callable only by the contract owner.
153
+ /// Emits the `UpdateMaxSupply` event.
154
+ function updateMaxSupply(uint256 _maxSupply) external onlyOwner {
155
+ uint256 oldMaxSupply = maxSupply;
156
+ require(_maxSupply > oldMaxSupply, "AnnotatedMintNft: old supply less than new supply");
157
+
158
+ maxSupply = _maxSupply;
159
+ emit UpdateMaxSupply(oldMaxSupply, _maxSupply);
160
+ }
161
+
162
+ /// @dev Returns true if specified `_tokenId` exists.
163
+ function exists(uint256 _tokenId) external view returns (bool) {
164
+ return _ownerOf(_tokenId) != address(0);
165
+ }
166
+
167
+ /// @dev Returns true if `_account` is in the mapping of allowed `minters`.
168
+ function isMinter(address _account) public view returns (bool) {
169
+ return minters[_account];
170
+ }
171
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ pragma solidity ^0.8.20;
4
+
5
+ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6
+
7
+ contract BaseState {
8
+ /// @dev Required payment for the NFT in sale.
9
+ uint256 public nftPrice;
10
+
11
+ /// @dev True if contract has been initialized via `initialize` function.
12
+ bool public initialized;
13
+
14
+ /// @dev Address of the NFT for sale.
15
+ address public nftAddress;
16
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ pragma solidity ^0.8.20;
4
+
5
+ import "@openzeppelin/contracts/utils/Address.sol";
6
+ import "@openzeppelin/contracts/utils/StorageSlot.sol";
7
+
8
+ /// @title ERC1967
9
+ /// @dev ERC1967 contract providing randomized implementation storage slot
10
+ /// Made as a small subset of https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/ERC1967/ERC1967Upgrade.sol
11
+ abstract contract ERC1967 {
12
+ /**
13
+ * @dev Storage slot with the address of the current implementation.
14
+ * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
15
+ * validated in the constructor.
16
+ */
17
+ bytes32 internal constant _IMPLEMENTATION_SLOT =
18
+ 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
19
+
20
+ /**
21
+ * @dev Assert that value of `_IMPLEMENTATION_SLOT` matches expected value
22
+ */
23
+ function _assertCorrectImplementationSlot() internal pure {
24
+ assert(
25
+ _IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
26
+ );
27
+ }
28
+
29
+ /**
30
+ * @dev Returns the current implementation address.
31
+ */
32
+ function _getImplementation() internal view returns (address) {
33
+ return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
34
+ }
35
+
36
+ /**
37
+ * @dev Stores a new address in the EIP1967 implementation slot.
38
+ */
39
+ function _setImplementation(address newImplementation) internal {
40
+ require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract");
41
+ StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
42
+ }
43
+ }
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ pragma solidity ^0.8.20;
4
+
5
+ import "./AnnotatedMintNft.sol";
6
+ import "./State.sol";
7
+ import "./ERC1967.sol";
8
+
9
+ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
10
+
11
+ /// @dev Facilitates selling NFTs for specific ERC20s that accepts extra data when buying for any initialization data needed in a Paima dApp.
12
+ contract Erc20NftSale is State, ERC1967, OwnableUpgradeable {
13
+ /// @dev Emitted when the contract is initialized.
14
+ event Initialized(ERC20[] indexed currencies, address indexed owner, address indexed nft);
15
+
16
+ /// @dev Emitted when the NFT price is updated from `oldPrice` to `newPrice`.
17
+ event UpdatePrice(uint256 indexed oldPrice, uint256 indexed newPrice);
18
+
19
+ /// @dev Emitted when the `token` is removed from the list of `supportedCurrencies`.
20
+ event RemoveWhitelistedToken(address indexed token);
21
+
22
+ /// @dev Emitted when the `tokens` are added as the `supportedCurrencies`.
23
+ event WhitelistTokens(address[] indexed tokens);
24
+
25
+ /// @dev Emitted when an NFT of `tokenId` is minted to `receiver` by `buyer` paying `PRICE` in tokens of `supportedCurrencies`.
26
+ event BuyWithToken(
27
+ uint256 indexed tokenId,
28
+ uint256 indexed PRICE,
29
+ address indexed receiver,
30
+ address buyer
31
+ );
32
+
33
+ /// @custom:oz-upgrades-unsafe-allow constructor
34
+ constructor() {
35
+ _disableInitializers();
36
+ }
37
+
38
+ /// @dev Initializes the contract with the requested price `_price` in tokens of `currencies` for specified NFT `_nft`,
39
+ /// transferring ownership to the specified `owner`.
40
+ /// Callable only once.
41
+ /// Emits the `Initialized` event.
42
+ function initialize(
43
+ ERC20[] memory currencies,
44
+ address owner,
45
+ address _nft,
46
+ uint256 _price
47
+ ) public virtual initializer {
48
+ require(!initialized, "Contract already initialized");
49
+ initialized = true;
50
+
51
+ for (uint8 i = 0; i < currencies.length; i++) {
52
+ supportedCurrencies.push(currencies[i]);
53
+ }
54
+
55
+ nftPrice = _price;
56
+ nftAddress = _nft;
57
+ __Ownable_init(owner);
58
+
59
+ emit Initialized(currencies, owner, _nft);
60
+ }
61
+
62
+ /// @dev Purchases NFT for address `receiverAddress`, paying required price in token of `_tokenAddress`,
63
+ /// if it is one of the `supportedCurrencies`.
64
+ /// This function calls the `mint` function on the `AnnotatedMintNft` contract, passing provided `initialData`.
65
+ /// Emits the `BuyWithToken` event.
66
+ function buyWithToken(
67
+ ERC20 _tokenAddress,
68
+ address receiverAddress,
69
+ // not calldata to allow other contracts to wrap this and change what is bought with their own logic
70
+ string memory initialData
71
+ ) public virtual returns (uint256) {
72
+ require(tokenIsWhitelisted(_tokenAddress), "Erc20NftSale: token not whitelisted");
73
+ require(receiverAddress != address(0), "Erc20NftSale: zero receiver address");
74
+
75
+ uint256 price = nftPrice;
76
+
77
+ // transfer tokens from buyer to contract
78
+ ERC20(_tokenAddress).transferFrom(msg.sender, address(this), price);
79
+
80
+ uint256 tokenId = AnnotatedMintNft(nftAddress).mint(receiverAddress, initialData);
81
+
82
+ if (!depositedCurrenciesMap[_tokenAddress]) {
83
+ depositedCurrencies.push(_tokenAddress);
84
+ depositedCurrenciesMap[_tokenAddress] = true;
85
+ }
86
+
87
+ emit BuyWithToken(tokenId, price, receiverAddress, msg.sender);
88
+
89
+ return tokenId;
90
+ }
91
+
92
+ /// @dev Removes `_token` from the list of `supportedCurrencies`.
93
+ /// Callable only by the contract owner.
94
+ /// Emits the `RemoveWhitelistedToken` event.
95
+ function removeWhitelistedToken(ERC20 _token) external onlyOwner {
96
+ require(tokenIsWhitelisted(_token), "Erc20NftSale: token not whitelisted");
97
+
98
+ ERC20[] memory supportedCurrenciesMem = supportedCurrencies;
99
+
100
+ uint256 tokenIndex;
101
+ for (uint256 i = 0; i < supportedCurrenciesMem.length; i++) {
102
+ if (supportedCurrenciesMem[i] == _token) {
103
+ tokenIndex = i;
104
+ break;
105
+ }
106
+ }
107
+
108
+ require(tokenIndex < supportedCurrenciesMem.length, "Erc20NftSale: out of bounds");
109
+
110
+ supportedCurrencies[tokenIndex] = supportedCurrencies[supportedCurrencies.length - 1];
111
+ supportedCurrencies.pop();
112
+
113
+ emit RemoveWhitelistedToken(address(_token));
114
+ }
115
+
116
+ /// @dev Adds `_tokens` to the `supportedCurrencies` array.
117
+ /// Callable only by the contract owner.
118
+ /// Emits the `WhitelistTokens` event.
119
+ function whitelistTokens(ERC20[] memory _tokens) external onlyOwner {
120
+ address[] memory newWhiteList = new address[](_tokens.length);
121
+
122
+ for (uint256 i = 0; i < _tokens.length; i++) {
123
+ newWhiteList[i] = address(_tokens[i]);
124
+ supportedCurrencies.push(_tokens[i]);
125
+ }
126
+
127
+ emit WhitelistTokens(newWhiteList);
128
+ }
129
+
130
+ /// @dev Changes the sale price to `_nftPrice`.
131
+ /// Callable only by the contract owner.
132
+ /// Emits the `UpdatePrice` event.
133
+ function updatePrice(uint256 _nftPrice) external onlyOwner {
134
+ uint256 oldPrice = nftPrice;
135
+ nftPrice = _nftPrice;
136
+
137
+ emit UpdatePrice(oldPrice, _nftPrice);
138
+ }
139
+
140
+ /// @dev Withdraws the contract balance of `depositedCurrencies` to `_account`.
141
+ /// Callable only by the contract owner.
142
+ function withdraw(address _account) external onlyOwner {
143
+ ERC20[] memory currencies = depositedCurrencies;
144
+
145
+ for (uint256 i = 0; i < currencies.length; ) {
146
+ uint256 balance = currencies[i].balanceOf(address(this));
147
+ if (balance > 0) {
148
+ currencies[i].transfer(_account, balance);
149
+ }
150
+
151
+ depositedCurrenciesMap[currencies[i]] = false;
152
+
153
+ unchecked {
154
+ i++;
155
+ }
156
+ }
157
+
158
+ delete depositedCurrencies;
159
+ }
160
+
161
+ /// @dev Upgrades the contract implementation to `_newContract`.
162
+ /// Callable only by the contract owner.
163
+ function upgradeContract(address _newContract) external onlyOwner {
164
+ _setImplementation(_newContract);
165
+ }
166
+
167
+ /// @dev Returns true if `_token` is part of the `supportedCurrencies`.
168
+ function tokenIsWhitelisted(ERC20 _token) public view returns (bool) {
169
+ ERC20[] memory supportedCurrenciesMem = supportedCurrencies;
170
+ for (uint256 i = 0; i < supportedCurrenciesMem.length; i++) {
171
+ if (supportedCurrenciesMem[i] == _token) return true;
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ /// @dev Returns the token addresses that have been used as a payment in the NFT purchases.
178
+ function getDepositedCurrencies() public view returns (ERC20[] memory) {
179
+ return depositedCurrencies;
180
+ }
181
+
182
+ /// @dev Returns an array of token addresses that are accepted as payment in the NFT purchase.
183
+ function getSupportedCurrencies() public view returns (ERC20[] memory) {
184
+ return supportedCurrencies;
185
+ }
186
+ }