From dbf6c04db2b8599a48c4b3db5e7ab0459b190614 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 24 Mar 2022 13:31:49 -0700 Subject: [PATCH] fix(utils): Port StringArrayUtils from set-v2-strategies [SIM-124] (#227) --- contracts/lib/StringArrayUtils.sol | 61 ++++++++++++ contracts/mocks/StringArrayUtilsMock.sol | 45 +++++++++ test/lib/stringArrayUtils.spec.ts | 121 +++++++++++++++++++++++ utils/contracts/index.ts | 1 + utils/deploys/deployMocks.ts | 6 ++ 5 files changed, 234 insertions(+) create mode 100644 contracts/lib/StringArrayUtils.sol create mode 100644 contracts/mocks/StringArrayUtilsMock.sol create mode 100644 test/lib/stringArrayUtils.spec.ts diff --git a/contracts/lib/StringArrayUtils.sol b/contracts/lib/StringArrayUtils.sol new file mode 100644 index 000000000..9ac97f96f --- /dev/null +++ b/contracts/lib/StringArrayUtils.sol @@ -0,0 +1,61 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; + +/** + * @title StringArrayUtils + * @author Set Protocol + * + * Utility functions to handle String Arrays + */ +library StringArrayUtils { + + /** + * Finds the index of the first occurrence of the given element. + * @param A The input string to search + * @param a The value to find + * @return Returns (index and isIn) for the first occurrence starting from index 0 + */ + function indexOf(string[] memory A, string memory a) internal pure returns (uint256, bool) { + uint256 length = A.length; + for (uint256 i = 0; i < length; i++) { + if (keccak256(bytes(A[i])) == keccak256(bytes(a))) { + return (i, true); + } + } + return (uint256(-1), false); + } + + /** + * @param A The input array to search + * @param a The string to remove + */ + function removeStorage(string[] storage A, string memory a) + internal + { + (uint256 index, bool isIn) = indexOf(A, a); + if (!isIn) { + revert("String not in array."); + } else { + uint256 lastIndex = A.length - 1; // If the array would be empty, the previous line would throw, so no underflow here + if (index != lastIndex) { A[index] = A[lastIndex]; } + A.pop(); + } + } +} diff --git a/contracts/mocks/StringArrayUtilsMock.sol b/contracts/mocks/StringArrayUtilsMock.sol new file mode 100644 index 000000000..8d44baaf3 --- /dev/null +++ b/contracts/mocks/StringArrayUtilsMock.sol @@ -0,0 +1,45 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { StringArrayUtils } from "../lib/StringArrayUtils.sol"; + + +contract StringArrayUtilsMock { + using StringArrayUtils for string[]; + + string[] public storageArray; + + function testIndexOf(string[] memory A, string memory a) external pure returns (uint256, bool) { + return A.indexOf(a); + } + + function testRemoveStorage(string memory a) external { + storageArray.removeStorage(a); + } + + function setStorageArray(string[] memory A) external { + storageArray = A; + } + + function getStorageArray() external view returns(string[] memory) { + return storageArray; + } +} diff --git a/test/lib/stringArrayUtils.spec.ts b/test/lib/stringArrayUtils.spec.ts new file mode 100644 index 000000000..5fd5ec2bf --- /dev/null +++ b/test/lib/stringArrayUtils.spec.ts @@ -0,0 +1,121 @@ +import "module-alias/register"; +import { BigNumber } from "ethers"; + +import { Address } from "@utils/types"; +import { MAX_UINT_256 } from "@utils/constants"; +import { StringArrayUtilsMock } from "@utils/contracts/index"; +import DeployHelper from "@utils/deploys"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getWaffleExpect, +} from "@utils/test/index"; + +const expect = getWaffleExpect(); + +describe("StringArrayUtils", () => { + let stringOne: string; + let stringTwo: string; + let stringThree: string; + let unincludedString: string; + let deployer: DeployHelper; + + let stringArrayUtils: StringArrayUtilsMock; + + let baseArray: Address[]; + + before(async () => { + + stringOne = "eth"; + stringTwo = "to"; + stringThree = "$10k"; + + unincludedString = "$0"; + + const [ owner ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + stringArrayUtils = await deployer.mocks.deployStringArrayUtilsMock(); + + baseArray = [ stringOne, stringTwo, stringThree ]; + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("#indexOf", async () => { + let subjectArray: string[]; + let subjectString: string; + + beforeEach(async () => { + subjectArray = baseArray; + subjectString = stringTwo; + }); + + async function subject(): Promise { + return stringArrayUtils.testIndexOf(subjectArray, subjectString); + } + + it("should return the correct index and true", async () => { + const [index, isIn] = await subject(); + + expect(index).to.eq(BigNumber.from(1)); + expect(isIn).to.be.true; + }); + + describe("when passed address is not in array", async () => { + beforeEach(async () => { + subjectString = unincludedString; + }); + + it("should return false and max number index", async () => { + const [index, isIn] = await subject(); + + expect(index).to.eq(MAX_UINT_256); + expect(isIn).to.be.false; + }); + }); + }); + + describe("#removeStorage", async () => { + let subjectString: string; + + beforeEach(async () => { + await stringArrayUtils.setStorageArray(baseArray); + subjectString = stringTwo; + }); + + async function subject(): Promise { + return stringArrayUtils.testRemoveStorage(subjectString); + } + + it("should make the correct updates to the storage array", async () => { + await subject(); + + const actualArray = await stringArrayUtils.getStorageArray(); + expect(JSON.stringify(actualArray)).to.eq(JSON.stringify([ stringOne, stringThree ])); + }); + + describe("when item being removed is last in array", async () => { + beforeEach(async () => { + subjectString = stringThree; + }); + + it("should just pop off last item", async () => { + await subject(); + + const actualArray = await stringArrayUtils.getStorageArray(); + expect(JSON.stringify(actualArray)).to.eq(JSON.stringify([ stringOne, stringTwo ])); + }); + }); + + describe("when passed address is not in array", async () => { + beforeEach(async () => { + subjectString = unincludedString; + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("String not in array."); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 7bb428b03..f015d6aec 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -89,6 +89,7 @@ export { StakingRewards } from "../../typechain/StakingRewards"; export { StandardTokenMock } from "../../typechain/StandardTokenMock"; export { StandardTokenWithRoundingErrorMock } from "../../typechain/StandardTokenWithRoundingErrorMock"; export { StandardTokenWithFeeMock } from "../../typechain/StandardTokenWithFeeMock"; +export { StringArrayUtilsMock } from "../../typechain/StringArrayUtilsMock"; export { StreamingFeeModule } from "../../typechain/StreamingFeeModule"; export { SynthetixExchangeAdapter } from "../../typechain/SynthetixExchangeAdapter"; export { SynthetixExchangerMock } from "../../typechain/SynthetixExchangerMock"; diff --git a/utils/deploys/deployMocks.ts b/utils/deploys/deployMocks.ts index c36c7c8a6..8d329161f 100644 --- a/utils/deploys/deployMocks.ts +++ b/utils/deploys/deployMocks.ts @@ -43,6 +43,7 @@ import { StandardTokenWithRoundingErrorMock, StandardTokenWithFeeMock, TradeAdapterMock, + StringArrayUtilsMock, SynthMock, SynthetixExchangerMock, Uint256ArrayUtilsMock, @@ -104,6 +105,7 @@ import { Uint256ArrayUtilsMock__factory } from "../../typechain/factories/Uint25 import { WrapAdapterMock__factory } from "../../typechain/factories/WrapAdapterMock__factory"; import { WrapV2AdapterMock__factory } from "../../typechain/factories/WrapV2AdapterMock__factory"; import { ZeroExMock__factory } from "../../typechain/factories/ZeroExMock__factory"; +import { StringArrayUtilsMock__factory } from "../../typechain/factories/StringArrayUtilsMock__factory"; import { SynthMock__factory } from "../../typechain/factories/SynthMock__factory"; import { SynthetixExchangerMock__factory } from "../../typechain/factories/SynthetixExchangerMock__factory"; import { YearnStrategyMock__factory } from "../../typechain/factories/YearnStrategyMock__factory"; @@ -445,6 +447,10 @@ export default class DeployMocks { return await new ChainlinkAggregatorMock__factory(this._deployerSigner).deploy(decimals); } + public async deployStringArrayUtilsMock(): Promise { + return await new StringArrayUtilsMock__factory(this._deployerSigner).deploy(); + } + /** *********************************** * Instance getters ************************************/