From ab133796535590db5fb234db34ccb93ddddf5785 Mon Sep 17 00:00:00 2001 From: Leo Arias Date: Thu, 6 Sep 2018 12:06:28 -0600 Subject: [PATCH 1/6] Improve encapsulation on TimedCrowdsale (#1281) * Improve encapsulation on ERC165 * Improve encapsulation on ERC20 * Improve encapsulation on ERC721 * Add tests, use standard getters * fix tests * Fix lint * move interface ids to implementation contracts * Do not prefix getters * Improve encapsulation on Crowdsales * add missing tests * remove only * Improve encapsulation on Pausable * add the underscore * Improve encapsulation on ownership * fix rebase * fix ownership * Improve encapsulation on payments * Add missing tests * add missing test * Do not prefix getters * Do not prefix getters * Fix tests. * Update modifiers to call public view functions. Fixes #1179. * remove isMinter * fix is owner call * fix isOpen * Fix merge * Improve encapsulation on TimedCrowdsale * Add missing parentheses * remove duplicate function definition --- .../price/IncreasingPriceCrowdsale.sol | 4 +-- .../crowdsale/validation/TimedCrowdsale.sol | 26 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol b/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol index 7cc07f083d5..ad448b319a4 100644 --- a/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol +++ b/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol @@ -49,8 +49,8 @@ contract IncreasingPriceCrowdsale is TimedCrowdsale { */ function getCurrentRate() public view returns (uint256) { // solium-disable-next-line security/no-block-members - uint256 elapsedTime = block.timestamp.sub(openingTime); - uint256 timeRange = closingTime.sub(openingTime); + uint256 elapsedTime = block.timestamp.sub(openingTime()); + uint256 timeRange = closingTime().sub(openingTime()); uint256 rateRange = initialRate_.sub(finalRate_); return initialRate_.sub(elapsedTime.mul(rateRange).div(timeRange)); } diff --git a/contracts/crowdsale/validation/TimedCrowdsale.sol b/contracts/crowdsale/validation/TimedCrowdsale.sol index 23f0e6e8702..c9726d9402b 100644 --- a/contracts/crowdsale/validation/TimedCrowdsale.sol +++ b/contracts/crowdsale/validation/TimedCrowdsale.sol @@ -11,8 +11,8 @@ import "../Crowdsale.sol"; contract TimedCrowdsale is Crowdsale { using SafeMath for uint256; - uint256 public openingTime; - uint256 public closingTime; + uint256 private openingTime_; + uint256 private closingTime_; /** * @dev Reverts if not in crowdsale time range. @@ -32,8 +32,22 @@ contract TimedCrowdsale is Crowdsale { require(_openingTime >= block.timestamp); require(_closingTime >= _openingTime); - openingTime = _openingTime; - closingTime = _closingTime; + openingTime_ = _openingTime; + closingTime_ = _closingTime; + } + + /** + * @return the crowdsale opening time. + */ + function openingTime() public view returns(uint256) { + return openingTime_; + } + + /** + * @return the crowdsale closing time. + */ + function closingTime() public view returns(uint256) { + return closingTime_; } /** @@ -41,7 +55,7 @@ contract TimedCrowdsale is Crowdsale { */ function isOpen() public view returns (bool) { // solium-disable-next-line security/no-block-members - return block.timestamp >= openingTime && block.timestamp <= closingTime; + return block.timestamp >= openingTime_ && block.timestamp <= closingTime_; } /** @@ -50,7 +64,7 @@ contract TimedCrowdsale is Crowdsale { */ function hasClosed() public view returns (bool) { // solium-disable-next-line security/no-block-members - return block.timestamp > closingTime; + return block.timestamp > closingTime_; } /** From 661e5d86dc3e2e4246ff4cdc41ed082d41c74f19 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 6 Sep 2018 19:37:14 -0300 Subject: [PATCH 2/6] Rename SupportsInterfaceWithLookup to ERC165 (#1288) * rename SupportsInterfaceWithLookup to ERC165 * rename SupportsInterfaceWithLookup files to ERC165 --- .../{SupportsInterfaceWithLookup.sol => ERC165.sol} | 4 ++-- .../{SupportsInterfaceWithLookupMock.sol => ERC165Mock.sol} | 4 ++-- contracts/token/ERC721/ERC721.sol | 4 ++-- contracts/token/ERC721/ERC721Basic.sol | 4 ++-- .../{SupportsInterfaceWithLookup.test.js => ERC165.test.js} | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename contracts/introspection/{SupportsInterfaceWithLookup.sol => ERC165.sol} (92%) rename contracts/mocks/{SupportsInterfaceWithLookupMock.sol => ERC165Mock.sol} (50%) rename test/introspection/{SupportsInterfaceWithLookup.test.js => ERC165.test.js} (66%) diff --git a/contracts/introspection/SupportsInterfaceWithLookup.sol b/contracts/introspection/ERC165.sol similarity index 92% rename from contracts/introspection/SupportsInterfaceWithLookup.sol rename to contracts/introspection/ERC165.sol index 6fca22ac8ca..70868ff676e 100644 --- a/contracts/introspection/SupportsInterfaceWithLookup.sol +++ b/contracts/introspection/ERC165.sol @@ -4,11 +4,11 @@ import "./IERC165.sol"; /** - * @title SupportsInterfaceWithLookup + * @title ERC165 * @author Matt Condon (@shrugs) * @dev Implements ERC165 using a lookup table. */ -contract SupportsInterfaceWithLookup is IERC165 { +contract ERC165 is IERC165 { bytes4 private constant InterfaceId_ERC165 = 0x01ffc9a7; /** diff --git a/contracts/mocks/SupportsInterfaceWithLookupMock.sol b/contracts/mocks/ERC165Mock.sol similarity index 50% rename from contracts/mocks/SupportsInterfaceWithLookupMock.sol rename to contracts/mocks/ERC165Mock.sol index e1c95fc07f0..63a39bbb27e 100644 --- a/contracts/mocks/SupportsInterfaceWithLookupMock.sol +++ b/contracts/mocks/ERC165Mock.sol @@ -1,9 +1,9 @@ pragma solidity ^0.4.24; -import "../introspection/SupportsInterfaceWithLookup.sol"; +import "../introspection/ERC165.sol"; -contract SupportsInterfaceWithLookupMock is SupportsInterfaceWithLookup { +contract ERC165Mock is ERC165 { function registerInterface(bytes4 _interfaceId) public { diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index a2c63ef5527..ca6d7b89d22 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -2,7 +2,7 @@ pragma solidity ^0.4.24; import "./IERC721.sol"; import "./ERC721Basic.sol"; -import "../../introspection/SupportsInterfaceWithLookup.sol"; +import "../../introspection/ERC165.sol"; /** @@ -11,7 +11,7 @@ import "../../introspection/SupportsInterfaceWithLookup.sol"; * Moreover, it includes approve all functionality using operator terminology * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md */ -contract ERC721 is SupportsInterfaceWithLookup, ERC721Basic, IERC721 { +contract ERC721 is ERC165, ERC721Basic, IERC721 { // Token name string internal name_; diff --git a/contracts/token/ERC721/ERC721Basic.sol b/contracts/token/ERC721/ERC721Basic.sol index fcc8397ea87..cf09e81786d 100644 --- a/contracts/token/ERC721/ERC721Basic.sol +++ b/contracts/token/ERC721/ERC721Basic.sol @@ -4,14 +4,14 @@ import "./IERC721Basic.sol"; import "./IERC721Receiver.sol"; import "../../math/SafeMath.sol"; import "../../utils/Address.sol"; -import "../../introspection/SupportsInterfaceWithLookup.sol"; +import "../../introspection/ERC165.sol"; /** * @title ERC721 Non-Fungible Token Standard basic implementation * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md */ -contract ERC721Basic is SupportsInterfaceWithLookup, IERC721Basic { +contract ERC721Basic is ERC165, IERC721Basic { using SafeMath for uint256; using Address for address; diff --git a/test/introspection/SupportsInterfaceWithLookup.test.js b/test/introspection/ERC165.test.js similarity index 66% rename from test/introspection/SupportsInterfaceWithLookup.test.js rename to test/introspection/ERC165.test.js index e8bccdf4345..37a24637d36 100644 --- a/test/introspection/SupportsInterfaceWithLookup.test.js +++ b/test/introspection/ERC165.test.js @@ -1,14 +1,14 @@ const { shouldSupportInterfaces } = require('./SupportsInterface.behavior'); const { assertRevert } = require('../helpers/assertRevert'); -const SupportsInterfaceWithLookup = artifacts.require('SupportsInterfaceWithLookupMock'); +const ERC165 = artifacts.require('ERC165Mock'); require('chai') .should(); -contract('SupportsInterfaceWithLookup', function () { +contract('ERC165', function () { beforeEach(async function () { - this.mock = await SupportsInterfaceWithLookup.new(); + this.mock = await ERC165.new(); }); it('does not allow 0xffffffff', async function () { From a7ee54e1991ea776625ba69bca5152f3e45d918c Mon Sep 17 00:00:00 2001 From: Bing Yi Date: Fri, 7 Sep 2018 06:43:48 +0800 Subject: [PATCH 3/6] getApproved require the given token ID exist (#1256) * getApproved require the given token ID exist * missing commit from merge * clarify the test descriptions --- contracts/token/ERC721/ERC721Basic.sol | 2 ++ test/token/ERC721/ERC721MintBurn.behavior.js | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC721/ERC721Basic.sol b/contracts/token/ERC721/ERC721Basic.sol index cf09e81786d..866640a0f4d 100644 --- a/contracts/token/ERC721/ERC721Basic.sol +++ b/contracts/token/ERC721/ERC721Basic.sol @@ -93,10 +93,12 @@ contract ERC721Basic is ERC165, IERC721Basic { /** * @dev Gets the approved address for a token ID, or zero if no address set + * Reverts if the token ID does not exist. * @param _tokenId uint256 ID of the token to query the approval of * @return address currently approved for the given token ID */ function getApproved(uint256 _tokenId) public view returns (address) { + require(_exists(_tokenId)); return tokenApprovals_[_tokenId]; } diff --git a/test/token/ERC721/ERC721MintBurn.behavior.js b/test/token/ERC721/ERC721MintBurn.behavior.js index c165f170356..ad7d207363d 100644 --- a/test/token/ERC721/ERC721MintBurn.behavior.js +++ b/test/token/ERC721/ERC721MintBurn.behavior.js @@ -84,15 +84,17 @@ function shouldBehaveLikeMintAndBurnERC721 (accounts) { }); }); - describe('when there is a previous approval', function () { + describe('when there is a previous approval burned', function () { beforeEach(async function () { await this.token.approve(accounts[1], tokenId, { from: sender }); const result = await this.token.burn(tokenId, { from: sender }); logs = result.logs; }); - it('clears the approval', async function () { - (await this.token.getApproved(tokenId)).should.be.equal(ZERO_ADDRESS); + context('getApproved', function () { + it('reverts', async function () { + await assertRevert(this.token.getApproved(tokenId)); + }); }); }); From df1fab540eae0510873bdc212528e740835aae0a Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 6 Sep 2018 21:48:04 -0300 Subject: [PATCH 4/6] rename MerkleProof.verifyProof to MerkleProof.verify (#1294) --- contracts/cryptography/MerkleProof.sol | 2 +- contracts/mocks/MerkleProofWrapper.sol | 4 ++-- test/library/MerkleProof.test.js | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/cryptography/MerkleProof.sol b/contracts/cryptography/MerkleProof.sol index 26a8b350c93..e46d08de597 100644 --- a/contracts/cryptography/MerkleProof.sol +++ b/contracts/cryptography/MerkleProof.sol @@ -14,7 +14,7 @@ library MerkleProof { * @param _root Merkle root * @param _leaf Leaf of Merkle tree */ - function verifyProof( + function verify( bytes32[] _proof, bytes32 _root, bytes32 _leaf diff --git a/contracts/mocks/MerkleProofWrapper.sol b/contracts/mocks/MerkleProofWrapper.sol index fe72d75f87f..5458acc94ea 100644 --- a/contracts/mocks/MerkleProofWrapper.sol +++ b/contracts/mocks/MerkleProofWrapper.sol @@ -5,7 +5,7 @@ import { MerkleProof } from "../cryptography/MerkleProof.sol"; contract MerkleProofWrapper { - function verifyProof( + function verify( bytes32[] _proof, bytes32 _root, bytes32 _leaf @@ -14,6 +14,6 @@ contract MerkleProofWrapper { pure returns (bool) { - return MerkleProof.verifyProof(_proof, _root, _leaf); + return MerkleProof.verify(_proof, _root, _leaf); } } diff --git a/test/library/MerkleProof.test.js b/test/library/MerkleProof.test.js index cba5d69f5f1..6674880b742 100644 --- a/test/library/MerkleProof.test.js +++ b/test/library/MerkleProof.test.js @@ -13,7 +13,7 @@ contract('MerkleProof', function () { merkleProof = await MerkleProofWrapper.new(); }); - describe('verifyProof', function () { + describe('verify', function () { it('should return true for a valid Merkle proof', async function () { const elements = ['a', 'b', 'c', 'd']; const merkleTree = new MerkleTree(elements); @@ -24,7 +24,7 @@ contract('MerkleProof', function () { const leaf = bufferToHex(sha3(elements[0])); - (await merkleProof.verifyProof(proof, root, leaf)).should.equal(true); + (await merkleProof.verify(proof, root, leaf)).should.equal(true); }); it('should return false for an invalid Merkle proof', async function () { @@ -40,7 +40,7 @@ contract('MerkleProof', function () { const badProof = badMerkleTree.getHexProof(badElements[0]); - (await merkleProof.verifyProof(badProof, correctRoot, correctLeaf)).should.equal(false); + (await merkleProof.verify(badProof, correctRoot, correctLeaf)).should.equal(false); }); it('should return false for a Merkle proof of invalid length', async function () { @@ -54,7 +54,7 @@ contract('MerkleProof', function () { const leaf = bufferToHex(sha3(elements[0])); - (await merkleProof.verifyProof(badProof, root, leaf)).should.equal(false); + (await merkleProof.verify(badProof, root, leaf)).should.equal(false); }); }); }); From 897ef293949794a88f3c789d9c27ea1edb077a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 6 Sep 2018 22:20:49 -0300 Subject: [PATCH 5/6] Bumped required solidity version to 0.4.24. (#1295) --- contracts/drafts/ERC1046/TokenMetadata.sol | 2 +- contracts/mocks/ERC20WithMetadataMock.sol | 2 +- contracts/payment/ConditionalEscrow.sol | 2 +- contracts/payment/Escrow.sol | 2 +- contracts/payment/RefundEscrow.sol | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/drafts/ERC1046/TokenMetadata.sol b/contracts/drafts/ERC1046/TokenMetadata.sol index 6efb4ed72db..31fe31029ed 100644 --- a/contracts/drafts/ERC1046/TokenMetadata.sol +++ b/contracts/drafts/ERC1046/TokenMetadata.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.21; +pragma solidity ^0.4.24; import "../../token/ERC20/IERC20.sol"; diff --git a/contracts/mocks/ERC20WithMetadataMock.sol b/contracts/mocks/ERC20WithMetadataMock.sol index e42e46f9697..c7ad978f827 100644 --- a/contracts/mocks/ERC20WithMetadataMock.sol +++ b/contracts/mocks/ERC20WithMetadataMock.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.21; +pragma solidity ^0.4.24; import "../token/ERC20/ERC20.sol"; import "../drafts/ERC1046/TokenMetadata.sol"; diff --git a/contracts/payment/ConditionalEscrow.sol b/contracts/payment/ConditionalEscrow.sol index 9278a69a39e..7108911b587 100644 --- a/contracts/payment/ConditionalEscrow.sol +++ b/contracts/payment/ConditionalEscrow.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "./Escrow.sol"; diff --git a/contracts/payment/Escrow.sol b/contracts/payment/Escrow.sol index dc3fbfef57b..ca0c124d773 100644 --- a/contracts/payment/Escrow.sol +++ b/contracts/payment/Escrow.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "../math/SafeMath.sol"; import "../ownership/Ownable.sol"; diff --git a/contracts/payment/RefundEscrow.sol b/contracts/payment/RefundEscrow.sol index 802dd471128..884494f1a8e 100644 --- a/contracts/payment/RefundEscrow.sol +++ b/contracts/payment/RefundEscrow.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "./ConditionalEscrow.sol"; import "../ownership/Ownable.sol"; From bf956024046748d2b40c473b077cdfbebbf4610d Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 7 Sep 2018 00:40:12 -0300 Subject: [PATCH 6/6] Simplify IndividuallyCappedCrowdsale interface (#1296) * remove concept of User and remove setGroupCap * fix linting error * remove mention of users from comments --- .../IndividuallyCappedCrowdsale.sol | 39 +++++------------ .../IndividuallyCappedCrowdsale.test.js | 43 ++----------------- 2 files changed, 15 insertions(+), 67 deletions(-) diff --git a/contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol b/contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol index f0c3fdd4c43..7d139edb886 100644 --- a/contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol +++ b/contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol @@ -7,7 +7,7 @@ import "../../ownership/Ownable.sol"; /** * @title IndividuallyCappedCrowdsale - * @dev Crowdsale with per-user caps. + * @dev Crowdsale with per-beneficiary caps. */ contract IndividuallyCappedCrowdsale is Ownable, Crowdsale { using SafeMath for uint256; @@ -16,53 +16,36 @@ contract IndividuallyCappedCrowdsale is Ownable, Crowdsale { mapping(address => uint256) private caps_; /** - * @dev Sets a specific user's maximum contribution. + * @dev Sets a specific beneficiary's maximum contribution. * @param _beneficiary Address to be capped * @param _cap Wei limit for individual contribution */ - function setUserCap(address _beneficiary, uint256 _cap) external onlyOwner { + function setCap(address _beneficiary, uint256 _cap) external onlyOwner { caps_[_beneficiary] = _cap; } /** - * @dev Sets a group of users' maximum contribution. - * @param _beneficiaries List of addresses to be capped - * @param _cap Wei limit for individual contribution - */ - function setGroupCap( - address[] _beneficiaries, - uint256 _cap - ) - external - onlyOwner - { - for (uint256 i = 0; i < _beneficiaries.length; i++) { - caps_[_beneficiaries[i]] = _cap; - } - } - - /** - * @dev Returns the cap of a specific user. + * @dev Returns the cap of a specific beneficiary. * @param _beneficiary Address whose cap is to be checked - * @return Current cap for individual user + * @return Current cap for individual beneficiary */ - function getUserCap(address _beneficiary) public view returns (uint256) { + function getCap(address _beneficiary) public view returns (uint256) { return caps_[_beneficiary]; } /** - * @dev Returns the amount contributed so far by a sepecific user. + * @dev Returns the amount contributed so far by a specific beneficiary. * @param _beneficiary Address of contributor - * @return User contribution so far + * @return Beneficiary contribution so far */ - function getUserContribution(address _beneficiary) + function getContribution(address _beneficiary) public view returns (uint256) { return contributions_[_beneficiary]; } /** - * @dev Extend parent behavior requiring purchase to respect the user's funding cap. + * @dev Extend parent behavior requiring purchase to respect the beneficiary's funding cap. * @param _beneficiary Token purchaser * @param _weiAmount Amount of wei contributed */ @@ -78,7 +61,7 @@ contract IndividuallyCappedCrowdsale is Ownable, Crowdsale { } /** - * @dev Extend parent behavior to update user contributions + * @dev Extend parent behavior to update beneficiary contributions * @param _beneficiary Token purchaser * @param _weiAmount Amount of wei contributed */ diff --git a/test/crowdsale/IndividuallyCappedCrowdsale.test.js b/test/crowdsale/IndividuallyCappedCrowdsale.test.js index 8deeca37700..b174c81bb62 100644 --- a/test/crowdsale/IndividuallyCappedCrowdsale.test.js +++ b/test/crowdsale/IndividuallyCappedCrowdsale.test.js @@ -23,8 +23,8 @@ contract('IndividuallyCappedCrowdsale', function ([_, wallet, alice, bob, charli beforeEach(async function () { this.token = await SimpleToken.new(); this.crowdsale = await CappedCrowdsale.new(rate, wallet, this.token.address); - await this.crowdsale.setUserCap(alice, capAlice); - await this.crowdsale.setUserCap(bob, capBob); + await this.crowdsale.setCap(alice, capAlice); + await this.crowdsale.setCap(bob, capBob); await this.token.transfer(this.crowdsale.address, tokenSupply); }); @@ -56,47 +56,12 @@ contract('IndividuallyCappedCrowdsale', function ([_, wallet, alice, bob, charli describe('reporting state', function () { it('should report correct cap', async function () { - (await this.crowdsale.getUserCap(alice)).should.be.bignumber.equal(capAlice); + (await this.crowdsale.getCap(alice)).should.be.bignumber.equal(capAlice); }); it('should report actual contribution', async function () { await this.crowdsale.buyTokens(alice, { value: lessThanCapAlice }); - (await this.crowdsale.getUserContribution(alice)).should.be.bignumber.equal(lessThanCapAlice); - }); - }); - }); - - describe('group capping', function () { - beforeEach(async function () { - this.token = await SimpleToken.new(); - this.crowdsale = await CappedCrowdsale.new(rate, wallet, this.token.address); - await this.crowdsale.setGroupCap([bob, charlie], capBob); - await this.token.transfer(this.crowdsale.address, tokenSupply); - }); - - describe('accepting payments', function () { - it('should accept payments within cap', async function () { - await this.crowdsale.buyTokens(bob, { value: lessThanCapBoth }); - await this.crowdsale.buyTokens(charlie, { value: lessThanCapBoth }); - }); - - it('should reject payments outside cap', async function () { - await this.crowdsale.buyTokens(bob, { value: capBob }); - await expectThrow(this.crowdsale.buyTokens(bob, { value: 1 }), EVMRevert); - await this.crowdsale.buyTokens(charlie, { value: capBob }); - await expectThrow(this.crowdsale.buyTokens(charlie, { value: 1 }), EVMRevert); - }); - - it('should reject payments that exceed cap', async function () { - await expectThrow(this.crowdsale.buyTokens(bob, { value: capBob.plus(1) }), EVMRevert); - await expectThrow(this.crowdsale.buyTokens(charlie, { value: capBob.plus(1) }), EVMRevert); - }); - }); - - describe('reporting state', function () { - it('should report correct cap', async function () { - (await this.crowdsale.getUserCap(bob)).should.be.bignumber.equal(capBob); - (await this.crowdsale.getUserCap(charlie)).should.be.bignumber.equal(capBob); + (await this.crowdsale.getContribution(alice)).should.be.bignumber.equal(lessThanCapAlice); }); }); });