Skip to content

Commit dbf6c04

Browse files
authored
fix(utils): Port StringArrayUtils from set-v2-strategies [SIM-124] (#227)
1 parent 9144c32 commit dbf6c04

File tree

5 files changed

+234
-0
lines changed

5 files changed

+234
-0
lines changed

contracts/lib/StringArrayUtils.sol

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2021 Set Labs Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache License, Version 2.0
17+
*/
18+
19+
pragma solidity 0.6.10;
20+
21+
/**
22+
* @title StringArrayUtils
23+
* @author Set Protocol
24+
*
25+
* Utility functions to handle String Arrays
26+
*/
27+
library StringArrayUtils {
28+
29+
/**
30+
* Finds the index of the first occurrence of the given element.
31+
* @param A The input string to search
32+
* @param a The value to find
33+
* @return Returns (index and isIn) for the first occurrence starting from index 0
34+
*/
35+
function indexOf(string[] memory A, string memory a) internal pure returns (uint256, bool) {
36+
uint256 length = A.length;
37+
for (uint256 i = 0; i < length; i++) {
38+
if (keccak256(bytes(A[i])) == keccak256(bytes(a))) {
39+
return (i, true);
40+
}
41+
}
42+
return (uint256(-1), false);
43+
}
44+
45+
/**
46+
* @param A The input array to search
47+
* @param a The string to remove
48+
*/
49+
function removeStorage(string[] storage A, string memory a)
50+
internal
51+
{
52+
(uint256 index, bool isIn) = indexOf(A, a);
53+
if (!isIn) {
54+
revert("String not in array.");
55+
} else {
56+
uint256 lastIndex = A.length - 1; // If the array would be empty, the previous line would throw, so no underflow here
57+
if (index != lastIndex) { A[index] = A[lastIndex]; }
58+
A.pop();
59+
}
60+
}
61+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2021 Set Labs Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache License, Version 2.0
17+
*/
18+
19+
pragma solidity 0.6.10;
20+
pragma experimental "ABIEncoderV2";
21+
22+
import { StringArrayUtils } from "../lib/StringArrayUtils.sol";
23+
24+
25+
contract StringArrayUtilsMock {
26+
using StringArrayUtils for string[];
27+
28+
string[] public storageArray;
29+
30+
function testIndexOf(string[] memory A, string memory a) external pure returns (uint256, bool) {
31+
return A.indexOf(a);
32+
}
33+
34+
function testRemoveStorage(string memory a) external {
35+
storageArray.removeStorage(a);
36+
}
37+
38+
function setStorageArray(string[] memory A) external {
39+
storageArray = A;
40+
}
41+
42+
function getStorageArray() external view returns(string[] memory) {
43+
return storageArray;
44+
}
45+
}

test/lib/stringArrayUtils.spec.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import "module-alias/register";
2+
import { BigNumber } from "ethers";
3+
4+
import { Address } from "@utils/types";
5+
import { MAX_UINT_256 } from "@utils/constants";
6+
import { StringArrayUtilsMock } from "@utils/contracts/index";
7+
import DeployHelper from "@utils/deploys";
8+
import {
9+
addSnapshotBeforeRestoreAfterEach,
10+
getAccounts,
11+
getWaffleExpect,
12+
} from "@utils/test/index";
13+
14+
const expect = getWaffleExpect();
15+
16+
describe("StringArrayUtils", () => {
17+
let stringOne: string;
18+
let stringTwo: string;
19+
let stringThree: string;
20+
let unincludedString: string;
21+
let deployer: DeployHelper;
22+
23+
let stringArrayUtils: StringArrayUtilsMock;
24+
25+
let baseArray: Address[];
26+
27+
before(async () => {
28+
29+
stringOne = "eth";
30+
stringTwo = "to";
31+
stringThree = "$10k";
32+
33+
unincludedString = "$0";
34+
35+
const [ owner ] = await getAccounts();
36+
37+
deployer = new DeployHelper(owner.wallet);
38+
stringArrayUtils = await deployer.mocks.deployStringArrayUtilsMock();
39+
40+
baseArray = [ stringOne, stringTwo, stringThree ];
41+
});
42+
43+
addSnapshotBeforeRestoreAfterEach();
44+
45+
describe("#indexOf", async () => {
46+
let subjectArray: string[];
47+
let subjectString: string;
48+
49+
beforeEach(async () => {
50+
subjectArray = baseArray;
51+
subjectString = stringTwo;
52+
});
53+
54+
async function subject(): Promise<any> {
55+
return stringArrayUtils.testIndexOf(subjectArray, subjectString);
56+
}
57+
58+
it("should return the correct index and true", async () => {
59+
const [index, isIn] = await subject();
60+
61+
expect(index).to.eq(BigNumber.from(1));
62+
expect(isIn).to.be.true;
63+
});
64+
65+
describe("when passed address is not in array", async () => {
66+
beforeEach(async () => {
67+
subjectString = unincludedString;
68+
});
69+
70+
it("should return false and max number index", async () => {
71+
const [index, isIn] = await subject();
72+
73+
expect(index).to.eq(MAX_UINT_256);
74+
expect(isIn).to.be.false;
75+
});
76+
});
77+
});
78+
79+
describe("#removeStorage", async () => {
80+
let subjectString: string;
81+
82+
beforeEach(async () => {
83+
await stringArrayUtils.setStorageArray(baseArray);
84+
subjectString = stringTwo;
85+
});
86+
87+
async function subject(): Promise<any> {
88+
return stringArrayUtils.testRemoveStorage(subjectString);
89+
}
90+
91+
it("should make the correct updates to the storage array", async () => {
92+
await subject();
93+
94+
const actualArray = await stringArrayUtils.getStorageArray();
95+
expect(JSON.stringify(actualArray)).to.eq(JSON.stringify([ stringOne, stringThree ]));
96+
});
97+
98+
describe("when item being removed is last in array", async () => {
99+
beforeEach(async () => {
100+
subjectString = stringThree;
101+
});
102+
103+
it("should just pop off last item", async () => {
104+
await subject();
105+
106+
const actualArray = await stringArrayUtils.getStorageArray();
107+
expect(JSON.stringify(actualArray)).to.eq(JSON.stringify([ stringOne, stringTwo ]));
108+
});
109+
});
110+
111+
describe("when passed address is not in array", async () => {
112+
beforeEach(async () => {
113+
subjectString = unincludedString;
114+
});
115+
116+
it("should revert", async () => {
117+
await expect(subject()).to.be.revertedWith("String not in array.");
118+
});
119+
});
120+
});
121+
});

utils/contracts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export { StakingRewards } from "../../typechain/StakingRewards";
8989
export { StandardTokenMock } from "../../typechain/StandardTokenMock";
9090
export { StandardTokenWithRoundingErrorMock } from "../../typechain/StandardTokenWithRoundingErrorMock";
9191
export { StandardTokenWithFeeMock } from "../../typechain/StandardTokenWithFeeMock";
92+
export { StringArrayUtilsMock } from "../../typechain/StringArrayUtilsMock";
9293
export { StreamingFeeModule } from "../../typechain/StreamingFeeModule";
9394
export { SynthetixExchangeAdapter } from "../../typechain/SynthetixExchangeAdapter";
9495
export { SynthetixExchangerMock } from "../../typechain/SynthetixExchangerMock";

utils/deploys/deployMocks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
StandardTokenWithRoundingErrorMock,
4444
StandardTokenWithFeeMock,
4545
TradeAdapterMock,
46+
StringArrayUtilsMock,
4647
SynthMock,
4748
SynthetixExchangerMock,
4849
Uint256ArrayUtilsMock,
@@ -104,6 +105,7 @@ import { Uint256ArrayUtilsMock__factory } from "../../typechain/factories/Uint25
104105
import { WrapAdapterMock__factory } from "../../typechain/factories/WrapAdapterMock__factory";
105106
import { WrapV2AdapterMock__factory } from "../../typechain/factories/WrapV2AdapterMock__factory";
106107
import { ZeroExMock__factory } from "../../typechain/factories/ZeroExMock__factory";
108+
import { StringArrayUtilsMock__factory } from "../../typechain/factories/StringArrayUtilsMock__factory";
107109
import { SynthMock__factory } from "../../typechain/factories/SynthMock__factory";
108110
import { SynthetixExchangerMock__factory } from "../../typechain/factories/SynthetixExchangerMock__factory";
109111
import { YearnStrategyMock__factory } from "../../typechain/factories/YearnStrategyMock__factory";
@@ -445,6 +447,10 @@ export default class DeployMocks {
445447
return await new ChainlinkAggregatorMock__factory(this._deployerSigner).deploy(decimals);
446448
}
447449

450+
public async deployStringArrayUtilsMock(): Promise<StringArrayUtilsMock> {
451+
return await new StringArrayUtilsMock__factory(this._deployerSigner).deploy();
452+
}
453+
448454
/** ***********************************
449455
* Instance getters
450456
************************************/

0 commit comments

Comments
 (0)