From 53b5bd23b9774d6eafcd3f59f82d357a7d53f442 Mon Sep 17 00:00:00 2001 From: Zoe Nolan Date: Wed, 25 Oct 2023 12:17:53 +0100 Subject: [PATCH 1/4] Switch to discount prices instead of percentages --- contracts/ExpandedNFT.sol | 108 +++++++++++++++++++++----------------- test/DiscountTest.ts | 32 ++++++++--- test/PriceTest.ts | 74 ++++---------------------- 3 files changed, 96 insertions(+), 118 deletions(-) diff --git a/contracts/ExpandedNFT.sol b/contracts/ExpandedNFT.sol index 2a65f01..0bd2e3f 100644 --- a/contracts/ExpandedNFT.sol +++ b/contracts/ExpandedNFT.sol @@ -102,15 +102,15 @@ contract ExpandedNFT is // Lifetime pass address IERC721Upgradeable lifetimePassAddress; - // Annual pass discount - uint256 annualPassDiscount; + // Annual pass price + uint256 annualPassAllowListPrice; + uint256 annualPassGeneralPrice; // Lifetime pass discount - uint256 lifetimePassDiscount; + uint256 lifetimePassAllowListPrice; + uint256 lifetimePassGeneralPrice; } - uint256 private constant _HUNDRED_PERCENT_AS_BPS = 10000; - // Artists wallet address address private _artistWallet; @@ -230,14 +230,24 @@ contract ExpandedNFT is return address(_pricing.lifetimePassAddress); } - /// @dev returns the Annual pass discount - function getAnnualPassDiscount() public view returns (uint256) { - return _pricing.annualPassDiscount; + /// @dev returns the Annual pass price + function getAnnualPassAllowListPrice() public view returns (uint256) { + return _pricing.annualPassAllowListPrice; + } + + /// @dev returns the Annual pass price + function getAnnualPassGeneralPrice() public view returns (uint256) { + return _pricing.annualPassGeneralPrice; } - /// @dev returns the Lifetime pass discount - function getLifetimePassDiscount() public view returns (uint256) { - return _pricing.lifetimePassDiscount; + /// @dev returns the Lifetime pass price + function getLifetimeAllowListPassPrice() public view returns (uint256) { + return _pricing.lifetimePassAllowListPrice; + } + + /// @dev returns the Lifetime pass price + function getLifetimePassGeneralPrice() public view returns (uint256) { + return _pricing.lifetimePassGeneralPrice; } /// @dev returns mint limit for the address @@ -269,15 +279,41 @@ contract ExpandedNFT is function allowListed(address wallet) public view returns (bool) { return _pricing.allowListMinters[wallet]; } - + /** @dev returns the current ETH sales price based on who can currently mint. */ function price() public view returns (uint256){ if (_pricing.whoCanMint == WhoCanMint.ALLOWLIST) { + // Assuming Lifetime passes have a greater or equal discount to the annual pass + if (address(_pricing.lifetimePassAddress) != address(0x0)) { + if (_pricing.lifetimePassAddress.balanceOf(msg.sender) > 0) { + return _pricing.lifetimePassAllowListPrice; + } + } + + if (address(_pricing.annualPassAddress) != address(0x0)) { + if (_pricing.annualPassAddress.balanceOf(msg.sender) > 0) { + return _pricing.annualPassAllowListPrice; + } + } + return _pricing.allowListSalePrice; } else if (_pricing.whoCanMint == WhoCanMint.ANYONE) { + // Assuming Lifetime passes have a greater or equal discount to the annual pass + if (address(_pricing.lifetimePassAddress) != address(0x0)) { + if (_pricing.lifetimePassAddress.balanceOf(msg.sender) > 0) { + return _pricing.lifetimePassGeneralPrice; + } + } + + if (address(_pricing.annualPassAddress) != address(0x0)) { + if (_pricing.annualPassAddress.balanceOf(msg.sender) > 0) { + return _pricing.annualPassGeneralPrice; + } + } + return salePrice; } @@ -356,33 +392,6 @@ contract ExpandedNFT is { uint256 paymentAmount = price() * numberToBeMinted; - // Assuming Lifetime passes have a greeater or equal discount to the annual pass - if (address(_pricing.lifetimePassAddress) != address(0x0)) { - if (_pricing.lifetimePassAddress.balanceOf(msg.sender) > 0) { - uint256 discount = _HUNDRED_PERCENT_AS_BPS - _pricing.lifetimePassDiscount; - uint256 lifetimePassPaymentAmount = (paymentAmount * discount) / _HUNDRED_PERCENT_AS_BPS; - - if (msg.value == lifetimePassPaymentAmount) { - return (true); - } - - return (false); - } - } - - if (address(_pricing.annualPassAddress) != address(0x0)) { - if (_pricing.annualPassAddress.balanceOf(msg.sender) > 0) { - uint256 discount = _HUNDRED_PERCENT_AS_BPS - _pricing.annualPassDiscount; - uint256 annualPassPaymentAmount = (paymentAmount * discount) / _HUNDRED_PERCENT_AS_BPS; - - if (msg.value == annualPassPaymentAmount) { - return (true); - } - - return (false); - } - } - if (msg.value == paymentAmount) { return (true); } @@ -481,18 +490,23 @@ contract ExpandedNFT is /** @param annualPassAddress Annual pass ERC721 token address. Can be null if no token is in use. @param lifetimePassAddress Lifetime pass ERC721 token address. Can be null if no token is in use. - @param annualPassDiscount Annual pass discount in BPS. - @param lifetimePassDiscount Lifetime pass discount in BPS. + @param annualPassAllowListPrice the allowlist price when holding an annual pass + @param annualPassGeneralPrice the general price when holding an annual pass + @param lifetimePassAllowListPrice the allowlist price when holding an lifetime pass + @param lifetimePassGeneralPrice the general price when holding an lifetime pass @dev Set various pricing related values */ - function updateDiscounts(address annualPassAddress, address lifetimePassAddress, uint256 annualPassDiscount, uint256 lifetimePassDiscount) external onlyOwner { - require(annualPassDiscount <= _HUNDRED_PERCENT_AS_BPS, "Discount can not be greater than 100%"); - require(lifetimePassDiscount <= _HUNDRED_PERCENT_AS_BPS, "Discount can not be greater than 100%"); - + function updateDiscounts(address annualPassAddress, address lifetimePassAddress, + uint256 annualPassAllowListPrice, uint256 annualPassGeneralPrice, + uint256 lifetimePassAllowListPrice, uint256 lifetimePassGeneralPrice) external onlyOwner { _pricing.annualPassAddress = IERC721Upgradeable(annualPassAddress); _pricing.lifetimePassAddress = IERC721Upgradeable(lifetimePassAddress); - _pricing.annualPassDiscount = annualPassDiscount; - _pricing.lifetimePassDiscount = lifetimePassDiscount; + + _pricing.annualPassAllowListPrice = annualPassAllowListPrice; + _pricing.annualPassGeneralPrice = annualPassGeneralPrice; + + _pricing.lifetimePassAllowListPrice = lifetimePassAllowListPrice; + _pricing.lifetimePassGeneralPrice = lifetimePassGeneralPrice; } /** diff --git a/test/DiscountTest.ts b/test/DiscountTest.ts index b2cb174..04b6ecd 100644 --- a/test/DiscountTest.ts +++ b/test/DiscountTest.ts @@ -32,9 +32,11 @@ describe("Discounts", () => { const mintCostAllowlist = ethers.utils.parseEther("0.4"); const mintCostGeneral = ethers.utils.parseEther("0.8"); - const hundreadPercentInBps = 10000; - const annualPassDiscountBps = 2500; - const lifetimePassDiscountBps = 5000; + const mintCostAllowlistLifetimePass = ethers.utils.parseEther("0.2"); + const mintCostGeneralLifetimePass = ethers.utils.parseEther("0.4"); + + const mintCostAllowlistAnnualPass = ethers.utils.parseEther("0.3"); + const mintCostGeneralAnnualPass = ethers.utils.parseEther("0.6"); beforeEach(async () => { signer = (await ethers.getSigners())[0]; @@ -84,13 +86,22 @@ describe("Discounts", () => { lifetimePassContract.initialize(); await minterContract.setPricing(10, 500, mintCostAllowlist, mintCostGeneral, 2, 1); - await minterContract.updateDiscounts(annualPassContract.address, lifetimePassContract.address, annualPassDiscountBps, lifetimePassDiscountBps); + const mintCostAllowlistLifetimePass = ethers.utils.parseEther("0.2"); + const mintCostGeneralLifetimePass = ethers.utils.parseEther("0.4"); + + const mintCostAllowlistAnnualPass = ethers.utils.parseEther("0.3"); + const mintCostGeneralAnnualPass = ethers.utils.parseEther("0.6"); + + + await minterContract.updateDiscounts(annualPassContract.address, lifetimePassContract.address, + mintCostAllowlistAnnualPass, mintCostGeneralAnnualPass, + mintCostAllowlistLifetimePass, mintCostGeneralLifetimePass); }); it("A non pass holder can not mint while the drop is not for sale", async () => { await minterContract.setAllowedMinter(0); - await expect(minterContract.connect(user).mintEditions([signerAddress], { value: ethers.utils.parseEther("0.4") })).to.be.revertedWith("Needs to be an allowed minter"); + await expect(minterContract.connect(user).mintEditions([signerAddress], { value: ethers.utils.parseEther("0.4") })).to.be.revertedWith("Needs to be an allowed minter"); }); it("A non pass holder can not mint while the drop is only for sale to allow listed wallets", async () => { @@ -342,7 +353,7 @@ describe("Discounts", () => { }); it("A pass holder can mint for free with 100% discount", async () => { - await minterContract.updateDiscounts(annualPassContract.address, lifetimePassContract.address, hundreadPercentInBps, hundreadPercentInBps); + await minterContract.updateDiscounts(annualPassContract.address, lifetimePassContract.address, 0, 0, 0, 0); await annualPassContract.connect(user).mint(userAddress); await minterContract.setAllowedMinter(2); @@ -357,8 +368,15 @@ describe("Discounts", () => { }); it("A pass holder can mint for the standard cost with 0% discount", async () => { - await minterContract.updateDiscounts(annualPassContract.address, lifetimePassContract.address, 0, 0); + const mintCostAllowlistLifetimePass = ethers.utils.parseEther("0.4"); + const mintCostGeneralLifetimePass = ethers.utils.parseEther("0.8"); + + const mintCostAllowlistAnnualPass = ethers.utils.parseEther("0.4"); + const mintCostGeneralAnnualPass = ethers.utils.parseEther("0.8"); + await minterContract.updateDiscounts(annualPassContract.address, lifetimePassContract.address, mintCostAllowlistAnnualPass, mintCostGeneralAnnualPass, + mintCostAllowlistLifetimePass, mintCostGeneralLifetimePass); + await annualPassContract.connect(user).mint(userAddress); await minterContract.setAllowedMinter(2); diff --git a/test/PriceTest.ts b/test/PriceTest.ts index 0b61d9f..62b78d7 100644 --- a/test/PriceTest.ts +++ b/test/PriceTest.ts @@ -98,7 +98,7 @@ describe("Pricing", () => { }); it("Set the discount not as the owner", async () => { - await expect(minterContract.connect(artist).updateDiscounts(nullAddress, nullAddress, 10, 10)).to.be.revertedWith("Ownable: caller is not the owner"); + await expect(minterContract.connect(artist).updateDiscounts(nullAddress, nullAddress, 10, 10, 10, 10)).to.be.revertedWith("Ownable: caller is not the owner"); }); it("Set the discount as the owner", async () => { @@ -107,72 +107,18 @@ describe("Pricing", () => { expect(await minterContract.getAnnualPassAddress()).to.be.equal(nullAddress); expect(await minterContract.getLifetimePassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(0); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(0); + expect(await minterContract.getAnnualPassAllowListPrice()).to.be.equal(0); + expect(await minterContract.getAnnualPassGeneralPrice()).to.be.equal(0); + expect(await minterContract.getLifetimeAllowListPassPrice()).to.be.equal(0); + expect(await minterContract.getLifetimePassGeneralPrice()).to.be.equal(0); - await minterContract.updateDiscounts(testAddress1, testAddress2, 10, 20); + await minterContract.updateDiscounts(testAddress1, testAddress2, 10, 20, 30, 40); expect(await minterContract.getAnnualPassAddress()).to.be.equal(testAddress1); expect(await minterContract.getLifetimePassAddress()).to.be.equal(testAddress2); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(10); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(20); - }); - - it("Set the annual pass discount to 100%", async () => { - const testAddress1 = "0x0123456789012345678901234567890123456789"; - const testAddress2 = "0x9876543210987654321098765432109876543210"; - - expect(await minterContract.getAnnualPassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getLifetimePassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(0); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(0); - - await minterContract.updateDiscounts(testAddress1, testAddress2, 10000, 20); - - expect(await minterContract.getAnnualPassAddress()).to.be.equal(testAddress1); - expect(await minterContract.getLifetimePassAddress()).to.be.equal(testAddress2); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(10000); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(20); - }); - - it("Set the annual pass discount to greater than 100%", async () => { - const testAddress1 = "0x0123456789012345678901234567890123456789"; - const testAddress2 = "0x9876543210987654321098765432109876543210"; - - expect(await minterContract.getAnnualPassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getLifetimePassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(0); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(0); - - await expect(minterContract.updateDiscounts(nullAddress, nullAddress, 10001, 10)).to.be.revertedWith("Discount can not be greater than 100%"); - }); - - it("Set the Lifetime pass discount to 100%", async () => { - const testAddress1 = "0x0123456789012345678901234567890123456789"; - const testAddress2 = "0x9876543210987654321098765432109876543210"; - - expect(await minterContract.getAnnualPassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getLifetimePassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(0); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(0); - - await minterContract.updateDiscounts(testAddress1, testAddress2, 20, 10000); - - expect(await minterContract.getAnnualPassAddress()).to.be.equal(testAddress1); - expect(await minterContract.getLifetimePassAddress()).to.be.equal(testAddress2); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(20); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(10000); - }); - - it("Set the Lifetime pass discount to greater than 100%", async () => { - const testAddress1 = "0x0123456789012345678901234567890123456789"; - const testAddress2 = "0x9876543210987654321098765432109876543210"; - - expect(await minterContract.getAnnualPassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getLifetimePassAddress()).to.be.equal(nullAddress); - expect(await minterContract.getAnnualPassDiscount()).to.be.equal(0); - expect(await minterContract.getLifetimePassDiscount()).to.be.equal(0); - - await expect(minterContract.updateDiscounts(nullAddress, nullAddress, 10, 10001)).to.be.revertedWith("Discount can not be greater than 100%"); + expect(await minterContract.getAnnualPassAllowListPrice()).to.be.equal(10); + expect(await minterContract.getAnnualPassGeneralPrice()).to.be.equal(20); + expect(await minterContract.getLifetimeAllowListPassPrice()).to.be.equal(30); + expect(await minterContract.getLifetimePassGeneralPrice()).to.be.equal(40); }); }); \ No newline at end of file From 872c9f335ab340252df0d3ea06303b7ed0d69e01 Mon Sep 17 00:00:00 2001 From: Zoe Nolan Date: Wed, 25 Oct 2023 12:39:02 +0100 Subject: [PATCH 2/4] Add test for removing allowlist wallets from the list --- test/AllowlistTest.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/AllowlistTest.ts b/test/AllowlistTest.ts index f5f1c0d..a5a670f 100644 --- a/test/AllowlistTest.ts +++ b/test/AllowlistTest.ts @@ -123,4 +123,46 @@ describe("Allow List", () => { expect((await minterContract.getAllowList()).toString()).to.be.equal([nullAddress].toString()); }); + it("Add a multiple wallets and remvoe the first to the allow list", async () => { + expect(await minterContract.allowListed(artistAddress)).to.be.equal(false); + + // Add a wallet to the allow list + await minterContract.setAllowListMinters(1, [artistAddress], [true]) + expect(await minterContract.allowListed(artistAddress)).to.be.equal(true); + expect(await minterContract.getAllowListCount()).to.be.equal(1); + expect((await minterContract.getAllowList()).toString()).to.be.equal([artistAddress].toString()); + + await minterContract.setAllowListMinters(1, [signerAddress], [true]) + expect(await minterContract.allowListed(signerAddress)).to.be.equal(true); + expect(await minterContract.getAllowListCount()).to.be.equal(2); + expect((await minterContract.getAllowList()).toString()).to.be.equal([artistAddress, signerAddress].toString()); + + // Remove a wallet to the allow list + await minterContract.setAllowListMinters(1, [artistAddress], [false]) + expect(await minterContract.allowListed(artistAddress)).to.be.equal(false); + expect(await minterContract.getAllowListCount()).to.be.equal(1); + expect((await minterContract.getAllowList()).toString()).to.be.equal([nullAddress, signerAddress].toString()); + }); + + it("Add a multiple wallets and remvoe the second to the allow list", async () => { + expect(await minterContract.allowListed(artistAddress)).to.be.equal(false); + + // Add a wallet to the allow list + await minterContract.setAllowListMinters(1, [artistAddress], [true]) + expect(await minterContract.allowListed(artistAddress)).to.be.equal(true); + expect(await minterContract.getAllowListCount()).to.be.equal(1); + expect((await minterContract.getAllowList()).toString()).to.be.equal([artistAddress].toString()); + + await minterContract.setAllowListMinters(1, [signerAddress], [true]) + expect(await minterContract.allowListed(signerAddress)).to.be.equal(true); + expect(await minterContract.getAllowListCount()).to.be.equal(2); + expect((await minterContract.getAllowList()).toString()).to.be.equal([artistAddress, signerAddress].toString()); + + // Remove a wallet to the allow list + await minterContract.setAllowListMinters(1, [signerAddress], [false]) + expect(await minterContract.allowListed(signerAddress)).to.be.equal(false); + expect(await minterContract.getAllowListCount()).to.be.equal(1); + expect((await minterContract.getAllowList()).toString()).to.be.equal([artistAddress, nullAddress].toString()); + }); + }); From 47f12457811fe2595ea08859982ae1ff0d0002b1 Mon Sep 17 00:00:00 2001 From: Zoe Nolan Date: Wed, 25 Oct 2023 12:45:15 +0100 Subject: [PATCH 3/4] Check allow list pricing without holding passes --- test/PriceTest.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/PriceTest.ts b/test/PriceTest.ts index 62b78d7..94d871d 100644 --- a/test/PriceTest.ts +++ b/test/PriceTest.ts @@ -10,6 +10,8 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { DropCreator, ExpandedNFT, + TestPassOne, + TestPassTwo, } from "../typechain"; describe("Pricing", () => { @@ -121,4 +123,40 @@ describe("Pricing", () => { expect(await minterContract.getLifetimeAllowListPassPrice()).to.be.equal(30); expect(await minterContract.getLifetimePassGeneralPrice()).to.be.equal(40); }); + + it("Check the price with valid passes addresses but not holding any", async () => { + await minterContract.setAllowedMinter(1); + + const { TestPassOne } = await deployments.fixture(["TestPassOne"]); + let annualPassContract = (await ethers.getContractAt( + "TestPassOne", + TestPassOne.address + )) as TestPassOne; + annualPassContract.initialize(); + + const { TestPassTwo } = await deployments.fixture(["TestPassTwo"]); + let lifetimePassContract = (await ethers.getContractAt( + "TestPassTwo", + TestPassTwo.address + )) as TestPassTwo; + lifetimePassContract.initialize(); + + expect(await minterContract.getAnnualPassAddress()).to.be.equal(nullAddress); + expect(await minterContract.getLifetimePassAddress()).to.be.equal(nullAddress); + expect(await minterContract.getAnnualPassAllowListPrice()).to.be.equal(0); + expect(await minterContract.getAnnualPassGeneralPrice()).to.be.equal(0); + expect(await minterContract.getLifetimeAllowListPassPrice()).to.be.equal(0); + expect(await minterContract.getLifetimePassGeneralPrice()).to.be.equal(0); + + await minterContract.updateDiscounts(annualPassContract.address, lifetimePassContract.address, 10, 20, 30, 40); + + expect(await minterContract.getAnnualPassAddress()).to.be.equal(annualPassContract.address); + expect(await minterContract.getLifetimePassAddress()).to.be.equal(lifetimePassContract.address); + expect(await minterContract.getAnnualPassAllowListPrice()).to.be.equal(10); + expect(await minterContract.getAnnualPassGeneralPrice()).to.be.equal(20); + expect(await minterContract.getLifetimeAllowListPassPrice()).to.be.equal(30); + expect(await minterContract.getLifetimePassGeneralPrice()).to.be.equal(40); + + expect(await minterContract.connect(artist).price()).to.be.equal(10); + }); }); \ No newline at end of file From d472f3153bf3ba5f293e57da5955970c9ffb13c8 Mon Sep 17 00:00:00 2001 From: Zoe Nolan Date: Wed, 25 Oct 2023 13:06:14 +0100 Subject: [PATCH 4/4] Package updates --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 872e6dc..2ed4316 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1295,9 +1295,9 @@ form-data "^4.0.0" "@types/node@*", "@types/node@^20.8.6": - version "20.8.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25" - integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ== + version "20.8.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" + integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== dependencies: undici-types "~5.25.1" @@ -3620,9 +3620,9 @@ hardhat-gas-reporter@^1.0.9: sha1 "^1.1.1" hardhat@^2.18.1: - version "2.18.2" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.18.2.tgz#e82169bafc83c4b2af9b33ac38bae6da5603074e" - integrity sha512-lUVmJg7DsKcUCDpqv57CJl6vHqo/1PeHSfM3+WIa8UtRKmXyVTj1qQK01TDiuetkZBVg9Dn52qU+ZwaJQynaKA== + version "2.18.3" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.18.3.tgz#8fd01348795c77086fff417a4d13c521dce28fcf" + integrity sha512-JuYaTG+4ZHVjEHCW5Hn6jCHH3LpO75dtgznZpM/dLv12RcSlw/xHbeQh3FAsGahQr1epKryZcZEMHvztVZHe0g== dependencies: "@ethersproject/abi" "^5.1.2" "@metamask/eth-sig-util" "^4.0.0"