diff --git a/.gitignore b/.gitignore index 9ff034ca..5fcc829e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ test-bor-docker/data coverage/ coverage.json + +scripts/helpers/interfaces +cache diff --git a/contracts/common/lib/ExitPayloadReader.sol b/contracts/common/lib/ExitPayloadReader.sol index c815f24d..90ab9030 100644 --- a/contracts/common/lib/ExitPayloadReader.sol +++ b/contracts/common/lib/ExitPayloadReader.sol @@ -1,6 +1,6 @@ pragma solidity 0.5.17; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "./RLPReader.sol"; import {BytesLib} from "./BytesLib.sol"; library ExitPayloadReader { diff --git a/contracts/common/lib/MerklePatriciaProof.sol b/contracts/common/lib/MerklePatriciaProof.sol index 2b34eb0f..08dec6a1 100644 --- a/contracts/common/lib/MerklePatriciaProof.sol +++ b/contracts/common/lib/MerklePatriciaProof.sol @@ -6,7 +6,7 @@ */ pragma solidity ^0.5.2; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "./RLPReader.sol"; library MerklePatriciaProof { /* diff --git a/contracts/common/lib/RLPReader.sol b/contracts/common/lib/RLPReader.sol new file mode 100644 index 00000000..3fae67cd --- /dev/null +++ b/contracts/common/lib/RLPReader.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * @author Hamdi Allam hamdi.allam97@gmail.com + * Please reach out with any questions or concerns + */ +pragma solidity >=0.5.10 <0.9.0; + +library RLPReader { + uint8 constant STRING_SHORT_START = 0x80; + uint8 constant STRING_LONG_START = 0xb8; + uint8 constant LIST_SHORT_START = 0xc0; + uint8 constant LIST_LONG_START = 0xf8; + uint8 constant WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + struct Iterator { + RLPItem item; // Item that's being iterated over. + uint256 nextPtr; // Position of the next item in the list. + } + + /* + * @dev Returns the next element in the iteration. Reverts if it has not next element. + * @param self The iterator. + * @return The next element in the iteration. + */ + function next(Iterator memory self) internal pure returns (RLPItem memory) { + require(hasNext(self)); + + uint256 ptr = self.nextPtr; + uint256 itemLength = _itemLength(ptr); + self.nextPtr = ptr + itemLength; + + return RLPItem(itemLength, ptr); + } + + /* + * @dev Returns true if the iteration has more elements. + * @param self The iterator. + * @return true if the iteration has more elements. + */ + function hasNext(Iterator memory self) internal pure returns (bool) { + RLPItem memory item = self.item; + return self.nextPtr < item.memPtr + item.len; + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + + return RLPItem(item.length, memPtr); + } + + /* + * @dev Create an iterator. Reverts if item is not a list. + * @param self The RLP item. + * @return An 'Iterator' over the item. + */ + function iterator(RLPItem memory self) internal pure returns (Iterator memory) { + require(isList(self)); + + uint256 ptr = self.memPtr + _payloadOffset(self.memPtr); + return Iterator(self, ptr); + } + + /* + * @param the RLP item. + */ + function rlpLen(RLPItem memory item) internal pure returns (uint256) { + return item.len; + } + + /* + * @param the RLP item. + * @return (memPtr, len) pair: location of the item's payload in memory. + */ + function payloadLocation(RLPItem memory item) internal pure returns (uint256, uint256) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 memPtr = item.memPtr + offset; + uint256 len = item.len - offset; // data length + return (memPtr, len); + } + + /* + * @param the RLP item. + */ + function payloadLen(RLPItem memory item) internal pure returns (uint256) { + (, uint256 len) = payloadLocation(item); + return len; + } + + /* + * @param the RLP item containing the encoded list. + */ + function toList(RLPItem memory item) internal pure returns (RLPItem[] memory) { + require(isList(item)); + + uint256 items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + // New check to see if the last mempointer of the last read item has moved farther than the parent item is long + require(memPtr - item.memPtr == item.len, "Wrong total length."); + + return result; + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + if (item.len == 0) return false; + + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + /* + * @dev A cheaper version of keccak256(toRlpBytes(item)) that avoids copying memory. + * @return keccak256 hash of RLP encoded bytes. + */ + function rlpBytesKeccak256(RLPItem memory item) internal pure returns (bytes32) { + uint256 ptr = item.memPtr; + uint256 len = item.len; + bytes32 result; + assembly { + result := keccak256(ptr, len) + } + return result; + } + + /* + * @dev A cheaper version of keccak256(toBytes(item)) that avoids copying memory. + * @return keccak256 hash of the item payload. + */ + function payloadKeccak256(RLPItem memory item) internal pure returns (bytes32) { + (uint256 memPtr, uint256 len) = payloadLocation(item); + bytes32 result; + assembly { + result := keccak256(memPtr, len) + } + return result; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + if (result.length == 0) return result; + + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + // any non-zero byte except "0x80" is considered true + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1); + uint256 result; + uint256 memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + + // SEE Github Issue #5. + // Summary: Most commonly used RLP libraries (i.e Geth) will encode + // "0" as "0x80" instead of as "0". We handle this edge case explicitly + // here. + if (result == 0 || result == STRING_SHORT_START) { + return false; + } else { + return true; + } + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix + require(item.len == 21); + + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + require(item.len > 0 && item.len <= 33); + + (uint256 memPtr, uint256 len) = payloadLocation(item); + + uint256 result; + assembly { + result := mload(memPtr) + + // shift to the correct location if neccesary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint256) { + // one byte prefix + require(item.len == 33); + + uint256 result; + uint256 memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + require(item.len > 0); + + (uint256 memPtr, uint256 len) = payloadLocation(item); + bytes memory result = new bytes(len); + + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(memPtr, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) private pure returns (uint256) { + if (item.len == 0) return 0; + + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) private pure returns (uint256) { + uint256 itemLen; + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) { + itemLen = 1; + } else if (byte0 < STRING_LONG_START) { + itemLen = byte0 - STRING_SHORT_START + 1; + } else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) private pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) { + return 0; + } else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) { + return 1; + } else if (byte0 < LIST_SHORT_START) { + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + } else { + return byte0 - (LIST_LONG_START - 1) + 1; + } + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy(uint256 src, uint256 dest, uint256 len) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + if (len > 0) { + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256**(WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } + } +} diff --git a/contracts/root/predicates/ERC20Predicate.sol b/contracts/root/predicates/ERC20Predicate.sol index c1fd8cff..e991d96c 100644 --- a/contracts/root/predicates/ERC20Predicate.sol +++ b/contracts/root/predicates/ERC20Predicate.sol @@ -4,7 +4,7 @@ import {BytesLib} from "../../common/lib/BytesLib.sol"; import {Common} from "../../common/lib/Common.sol"; import {Math} from "openzeppelin-solidity/contracts/math/Math.sol"; import {RLPEncode} from "../../common/lib/RLPEncode.sol"; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "../../common/lib/RLPReader.sol"; import {SafeMath} from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import {IErcPredicate} from "./IPredicate.sol"; diff --git a/contracts/root/predicates/ERC20PredicateBurnOnly.sol b/contracts/root/predicates/ERC20PredicateBurnOnly.sol index 54f0fb89..074d435e 100644 --- a/contracts/root/predicates/ERC20PredicateBurnOnly.sol +++ b/contracts/root/predicates/ERC20PredicateBurnOnly.sol @@ -4,7 +4,7 @@ import {BytesLib} from "../../common/lib/BytesLib.sol"; import {Common} from "../../common/lib/Common.sol"; import {Math} from "openzeppelin-solidity/contracts/math/Math.sol"; import {RLPEncode} from "../../common/lib/RLPEncode.sol"; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "../../common/lib/RLPReader.sol"; import {SafeMath} from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import {ExitPayloadReader} from "../../common/lib/ExitPayloadReader.sol"; import {IErcPredicate} from "./IPredicate.sol"; diff --git a/contracts/root/predicates/ERC721Predicate.sol b/contracts/root/predicates/ERC721Predicate.sol index 9628ff26..8db5912e 100644 --- a/contracts/root/predicates/ERC721Predicate.sol +++ b/contracts/root/predicates/ERC721Predicate.sol @@ -1,6 +1,6 @@ pragma solidity ^0.5.2; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "../../common/lib/RLPReader.sol"; import {SafeMath} from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import {BytesLib} from "../../common/lib/BytesLib.sol"; diff --git a/contracts/root/predicates/ERC721PredicateBurnOnly.sol b/contracts/root/predicates/ERC721PredicateBurnOnly.sol index 69fe26fa..84ff6b99 100644 --- a/contracts/root/predicates/ERC721PredicateBurnOnly.sol +++ b/contracts/root/predicates/ERC721PredicateBurnOnly.sol @@ -1,6 +1,6 @@ pragma solidity ^0.5.2; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "../../common/lib/RLPReader.sol"; import {SafeMath} from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import {BytesLib} from "../../common/lib/BytesLib.sol"; diff --git a/contracts/root/predicates/IPredicate.sol b/contracts/root/predicates/IPredicate.sol index a3c10760..114e3e3e 100644 --- a/contracts/root/predicates/IPredicate.sol +++ b/contracts/root/predicates/IPredicate.sol @@ -1,6 +1,6 @@ pragma solidity ^0.5.2; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "../../common/lib/RLPReader.sol"; import {Common} from "../../common/lib/Common.sol"; import {RLPEncode} from "../../common/lib/RLPEncode.sol"; diff --git a/contracts/root/predicates/MintableERC721Predicate.sol b/contracts/root/predicates/MintableERC721Predicate.sol index a9777e43..77c2ce38 100644 --- a/contracts/root/predicates/MintableERC721Predicate.sol +++ b/contracts/root/predicates/MintableERC721Predicate.sol @@ -1,6 +1,6 @@ pragma solidity ^0.5.2; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "../../common/lib/RLPReader.sol"; import {SafeMath} from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import {BytesLib} from "../../common/lib/BytesLib.sol"; diff --git a/contracts/root/withdrawManager/WithdrawManager.sol b/contracts/root/withdrawManager/WithdrawManager.sol index 724b3f2c..e2c9f385 100644 --- a/contracts/root/withdrawManager/WithdrawManager.sol +++ b/contracts/root/withdrawManager/WithdrawManager.sol @@ -3,8 +3,8 @@ pragma solidity ^0.5.2; import {ERC20} from "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; import {ERC721} from "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; import {Math} from "openzeppelin-solidity/contracts/math/Math.sol"; -import {RLPReader} from "solidity-rlp/contracts/RLPReader.sol"; +import {RLPReader} from "../../common/lib/RLPReader.sol"; import {Merkle} from "../../common/lib/Merkle.sol"; import {MerklePatriciaProof} from "../../common/lib/MerklePatriciaProof.sol"; import {PriorityQueue} from "../../common/lib/PriorityQueue.sol"; diff --git a/lib/forge-std b/lib/forge-std index 07263d19..1714bee7 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/package.json b/package.json index 8c952be0..c8c8a697 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test:ci": "scripts/run-test.sh", "testrpc": "ganache --chain.hardfork istanbul --wallet.mnemonic 'clock radar mass judge dismiss just intact mind resemble fringe diary casino' -p 8545 --gasLimit 10000000 --gasPrice 0 --chain.allowUnlimitedContractSize --accounts 200", "template:process": "node scripts/process-templates.cjs", + "generate:interfaces": "scripts/helpers/generateInterfaces.sh", "coverage": "LOCAL_NETWORK=true npx hardhat coverage --solcoverjs .solcover.cjs", "bor:simulate": "cd test-bor-docker && bash run-docker.sh", "bor:stop": "cd test-bor-docker && bash stop-docker.sh", diff --git a/scripts/deployers/DeployFix.s.sol b/scripts/deployers/DeployFix.s.sol new file mode 100644 index 00000000..13a77955 --- /dev/null +++ b/scripts/deployers/DeployFix.s.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Script, stdJson, console} from "forge-std/Script.sol"; + +// These imports get generated by npm run generate:interfaces +import {Governance} from "../helpers/interfaces/Governance.generated.sol"; +import {Registry} from "../helpers/interfaces/Registry.generated.sol"; +import {WithdrawManagerProxy} from "../helpers/interfaces/WithdrawManagerProxy.generated.sol"; + + +contract DeployFix is Script { + using stdJson for string; + + address registry = 0x33a02E6cC863D393d6Bf231B697b82F6e499cA71; + + // owned by 0xfa7d2a996ac6350f4b56c043112da0366a59b74c + address governance = 0x6e7a5820baD6cebA8Ef5ea69c0C92EbbDAc9CE48; + + address gSafeAddress = 0xFa7D2a996aC6350f4b56C043112Da0366a59b74c; + + // non upgradeable + // FxMintableERC20RootTunnel + // FxERC1155RootTunnel + // FxERC721RootTunnel + // FxERC20RootTunnel + // from pos-portal + + address depositManagerProxy = 0x401F6c983eA34274ec46f84D70b31C151321188b; + // owned by 0xfa7d2a996ac6350f4b56c043112da0366a59b74c + address withdrawManagerProxy = 0x2A88696e0fFA76bAA1338F2C74497cC013495922; + + address withdrawManager; + address eRC20PredicateBurnOnly; + address eRC721PredicateBurnOnly; + + + function run() public { + //uint256 deployerPrivateKey = vm.promptSecretUint("Enter deployer private key: "); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + + vm.startBroadcast(deployerPrivateKey); + + // deploy STEP 1 + // deploy new WithdrawManager version + withdrawManager = deployCode("out/WithdrawManager.sol/WithdrawManager.json"); + + console.log("deployed WithdrawManager implementation at: ", withdrawManager); + + // deploy STEP 2 + // deploy new ERC20PredicateBurnOnly version + eRC20PredicateBurnOnly = deployCode("out/ERC20PredicateBurnOnly.sol/ERC20PredicateBurnOnly.json", abi.encode(withdrawManagerProxy, depositManagerProxy)); + + console.log("deployed ERC20PredicateBurnOnly implementation at: ", eRC20PredicateBurnOnly); + + // deploy STEP 3 + // deploy new ERC721PredicateBurnOnly version + eRC721PredicateBurnOnly = deployCode("out/ERC721PredicateBurnOnly.sol/ERC721PredicateBurnOnly.json", abi.encode(withdrawManagerProxy, depositManagerProxy)); + + console.log("deployed ERC721PredicateBurnOnly implementation at: ", eRC721PredicateBurnOnly); + + + vm.stopBroadcast(); + + console.log("----------------------"); + console.log("Generating payloads \n"); + + // STEP 1 + // Remove predicates + bytes memory payloadRegistry1 = abi.encodeCall( + Governance.update, (address(registry), abi.encodeCall(Registry.removePredicate, (0x158d5fa3Ef8e4dDA8a5367deCF76b94E7efFCe95))) + ); + + console.log("Send: "); + console.logBytes(payloadRegistry1); + console.log("to: ", governance); + + bytes memory payloadRegistry2 = abi.encodeCall( + Governance.update, (address(registry), abi.encodeCall(Registry.removePredicate, (0x54150f44c785D412Ec262fe895Cc3B689c72F49B))) + ); + + console.log("Send: "); + console.logBytes(payloadRegistry2); + console.log("to: ", governance); + + // STEP 2 + // Add predicates + bytes memory payloadRegistry3 = abi.encodeCall( + Governance.update, (address(registry), abi.encodeCall(Registry.addErc20Predicate, (eRC20PredicateBurnOnly))) + ); + + console.log("Send: "); + console.logBytes(payloadRegistry3); + console.log("to: ", governance); + + bytes memory payloadRegistry4 = abi.encodeCall( + Governance.update, (address(registry), abi.encodeCall(Registry.addErc721Predicate, (eRC721PredicateBurnOnly))) + ); + + console.log("Send: "); + console.logBytes(payloadRegistry4); + console.log("to: ", governance); + + + // STEP 3 + // Update WithdrawManagerProxy implementation contract + bytes memory payloadWithdrawManager = abi.encodeCall(WithdrawManagerProxy.updateImplementation, (withdrawManager)); + + console.log("Send: "); + console.logBytes(payloadWithdrawManager); + console.log("to: ", withdrawManagerProxy); + } +} diff --git a/scripts/deployers/input.json b/scripts/deployers/input.json new file mode 100644 index 00000000..73fd7edd --- /dev/null +++ b/scripts/deployers/input.json @@ -0,0 +1,26 @@ +{ + "1": { + "registry": "0x33a02E6cC863D393d6Bf231B697b82F6e499cA71", + "stakeManagerProxy": "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908", + "depositManagerProxy": "0x401F6c983eA34274ec46f84D70b31C151321188b", + "governance": "0x6e7a5820baD6cebA8Ef5ea69c0C92EbbDAc9CE48", + "timelock": "0xCaf0aa768A3AE1297DF20072419Db8Bb8b5C8cEf", + "polToken": "0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6", + "matic": "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + "migration": "0x29e7DF7b6A1B2b07b731457f499E1696c60E2C4e", + "nativGasToken": "0x0000000000000000000000000000000000001010", + "gSafe": "0xFa7D2a996aC6350f4b56C043112Da0366a59b74c" + }, + "11155111": { + "registry": "0xfE92F7c3a701e43d8479738c8844bCc555b9e5CD", + "stakeManagerProxy": "0x4ae8f648b1ec892b6cc68c89cc088583964d08be", + "depositManagerProxy": "0x44Ad17990F9128C6d823Ee10dB7F0A5d40a731A4", + "timelock": "onlyMainnet", + "governance": "0xB7086eda3180c728C1536B35c4d54F6A2B33D6aC", + "polToken": "0x44499312f493F62f2DFd3C6435Ca3603EbFCeeBa", + "matic": "0x3fd0A53F4Bf853985a95F4Eb3F9C9FDE1F8e2b53", + "migration": "0x3A3B750E7d4d389Bc1d0be20E5D09530F82B9911", + "nativGasToken": "0x0000000000000000000000000000000000001010", + "gSafe": "onlyMainnet" + } +} \ No newline at end of file