ethers/lib/utils#solidityKeccak256 TypeScript Examples

The following examples show how to use ethers/lib/utils#solidityKeccak256. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts    From mStable-apps with GNU Lesser General Public License v3.0 7 votes vote down vote up
getProof = (balances: Record<string, string>, claimant: string): { balance: BigNumber; balanceSimple: number; proof: string[] } => {
  let claimantLeaf: string | undefined
  let claimantBalance: BigNumber | undefined

  const leaves = Object.entries(balances).map(([_account, _balance]) => {
    const balance = BigNumber.from(_balance)
    const leaf = solidityKeccak256(['address', 'uint256'], [_account, balance.toString()])

    if (!claimantLeaf && _account.toLowerCase() === claimant.toLowerCase()) {
      claimantLeaf = leaf
      claimantBalance = balance
    }

    return leaf
  })

  if (!claimantBalance || !claimantLeaf) throw new Error('Claim not found')

  const tree = new MerkleTree(leaves, hashFn, { sort: true })
  const proof = tree.getHexProof(claimantLeaf)

  const balanceSimple = parseFloat(formatUnits(claimantBalance))
  return { proof, balance: claimantBalance, balanceSimple }
}
Example #2
Source File: utils.ts    From hubble-contracts with MIT License 7 votes vote down vote up
export function computeRoot(
    leafInput: BytesLike,
    path: number,
    witness: BytesLike[]
) {
    let leaf = leafInput;
    for (let i = 0; i < witness.length; i++) {
        if (((path >> i) & 1) == 0) {
            leaf = solidityKeccak256(
                ["bytes32", "bytes32"],
                [leaf, witness[i]]
            );
        } else {
            leaf = solidityKeccak256(
                ["bytes32", "bytes32"],
                [witness[i], leaf]
            );
        }
    }
    return leaf;
}
Example #3
Source File: generator.ts    From merkle-airdrop-starter with GNU Affero General Public License v3.0 6 votes vote down vote up
/**
   * Generate Merkle Tree leaf from address and value
   * @param {string} address of airdrop claimee
   * @param {string} value of airdrop tokens to claimee
   * @returns {Buffer} Merkle Tree node
   */
  generateLeaf(address: string, value: string): Buffer {
    return Buffer.from(
      // Hash in appropriate Merkle format
      solidityKeccak256(["address", "uint256"], [address, value]).slice(2),
      "hex"
    );
  }
Example #4
Source File: balanceTree.ts    From index-coop-smart-contracts with Apache License 2.0 5 votes vote down vote up
// keccak256(abi.encode(index, account, amount))
  public static toNode(index: number | BigNumber, account: string, amount: BigNumber): Buffer {
    return Buffer.from(
      solidityKeccak256(["uint256", "address", "uint256"], [index, account, amount]).substr(2),
      "hex"
    );
  }
Example #5
Source File: BalancerTokenAdmin.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
MINTER_ROLE = solidityKeccak256(['string'], ['MINTER_ROLE'])
Example #6
Source File: BalancerTokenAdmin.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
SNAPSHOT_ROLE = solidityKeccak256(['string'], ['SNAPSHOT_ROLE'])
Example #7
Source File: merkleClaim.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
async function claimDistributions(numberOfDistributions: number, useInternalBalance: boolean) {
  console.log(`\n## ${useInternalBalance ? 'Using Internal Balance' : 'Sending and receiving tokens'}`);

  const merkleOrchard = await deploy('v2-distributors/MerkleOrchard', { args: [vault.address] });

  const token = tokens.first;
  const tokenAddresses = tokens.subset(1).addresses;
  const amount = BigNumber.from(100);
  const merkleLeaf = solidityKeccak256(['address', 'uint256'], [trader.address, amount]);

  const claims: Claim[] = Array.from({ length: numberOfDistributions }, (_, distribution) => ({
    distributionId: BigNumber.from(distribution),
    balance: amount,
    distributor: trader.address,
    tokenIndex: BigNumber.from(0),
    merkleProof: [],
  }));

  await token.approve(merkleOrchard.address, amount.mul(numberOfDistributions), { from: trader });
  for (let distribution = 0; distribution < numberOfDistributions; ++distribution) {
    await (
      await merkleOrchard.connect(trader).createDistribution(token.address, merkleLeaf, amount, distribution)
    ).wait();
  }

  let receipt;
  if (useInternalBalance) {
    receipt = await (
      await merkleOrchard.connect(trader).claimDistributionsToInternalBalance(trader.address, claims, tokenAddresses)
    ).wait();
  } else {
    receipt = await (
      await merkleOrchard.connect(trader).claimDistributions(trader.address, claims, tokenAddresses)
    ).wait();
  }

  console.log(
    `${numberOfDistributions} claims: ${printGas(receipt.gasUsed)} (${printGas(
      receipt.gasUsed / numberOfDistributions
    )} per claim)`
  );
}
Example #8
Source File: base.ts    From hubble-contracts with MIT License 5 votes vote down vote up
public hash(): string {
        return solidityKeccak256(
            ["bytes32", "bytes32"],
            [this.stateRoot, this.bodyRoot]
        );
    }
Example #9
Source File: pubkey.ts    From hubble-contracts with MIT License 5 votes vote down vote up
hashPubkey = (pubkey: solG2): string =>
    solidityKeccak256(solidityPubkeyType, pubkey)
Example #10
Source File: feeSplitExtension.spec.ts    From index-coop-smart-contracts with Apache License 2.0 4 votes vote down vote up
describe("FeeSplitExtension", () => {
  let owner: Account;
  let methodologist: Account;
  let operator: Account;
  let operatorFeeRecipient: Account;
  let setV2Setup: SetFixture;

  let deployer: DeployHelper;
  let setToken: SetToken;

  let baseManagerV2: BaseManagerV2;
  let feeExtension: FeeSplitExtension;

  before(async () => {
    [
      owner,
      methodologist,
      operator,
      operatorFeeRecipient,
    ] = await getAccounts();

    deployer = new DeployHelper(owner.wallet);

    setV2Setup = getSetFixture(owner.address);
    await setV2Setup.initialize();

    setToken = await setV2Setup.createSetToken(
      [setV2Setup.dai.address],
      [ether(1)],
      [setV2Setup.debtIssuanceModule.address, setV2Setup.streamingFeeModule.address]
    );

    // Deploy BaseManager
    baseManagerV2 = await deployer.manager.deployBaseManagerV2(
      setToken.address,
      operator.address,
      methodologist.address
    );
    await baseManagerV2.connect(methodologist.wallet).authorizeInitialization();

    const feeRecipient = baseManagerV2.address;
    const maxStreamingFeePercentage = ether(.1);
    const streamingFeePercentage = ether(.02);
    const streamingFeeSettings = {
      feeRecipient,
      maxStreamingFeePercentage,
      streamingFeePercentage,
      lastStreamingFeeTimestamp: ZERO,
    };
    await setV2Setup.streamingFeeModule.initialize(setToken.address, streamingFeeSettings);

    await setV2Setup.debtIssuanceModule.initialize(
      setToken.address,
      ether(.1),
      ether(.01),
      ether(.005),
      baseManagerV2.address,
      ADDRESS_ZERO
    );
  });

  addSnapshotBeforeRestoreAfterEach();

  describe("#constructor", async () => {
    let subjectManager: Address;
    let subjectStreamingFeeModule: Address;
    let subjectDebtIssuanceModule: Address;
    let subjectOperatorFeeSplit: BigNumber;
    let subjectOperatorFeeRecipient: Address;

    beforeEach(async () => {
      subjectManager = baseManagerV2.address;
      subjectStreamingFeeModule = setV2Setup.streamingFeeModule.address;
      subjectDebtIssuanceModule = setV2Setup.debtIssuanceModule.address;
      subjectOperatorFeeSplit = ether(.7);
      subjectOperatorFeeRecipient = operatorFeeRecipient.address;
    });

    async function subject(): Promise<FeeSplitExtension> {
      return await deployer.extensions.deployFeeSplitExtension(
        subjectManager,
        subjectStreamingFeeModule,
        subjectDebtIssuanceModule,
        subjectOperatorFeeSplit,
        subjectOperatorFeeRecipient
      );
    }

    it("should set the correct SetToken address", async () => {
      const feeExtension = await subject();

      const actualToken = await feeExtension.setToken();
      expect(actualToken).to.eq(setToken.address);
    });

    it("should set the correct manager address", async () => {
      const feeExtension = await subject();

      const actualManager = await feeExtension.manager();
      expect(actualManager).to.eq(baseManagerV2.address);
    });

    it("should set the correct streaming fee module address", async () => {
      const feeExtension = await subject();

      const actualStreamingFeeModule = await feeExtension.streamingFeeModule();
      expect(actualStreamingFeeModule).to.eq(subjectStreamingFeeModule);
    });

    it("should set the correct debt issuance module address", async () => {
      const feeExtension = await subject();

      const actualDebtIssuanceModule = await feeExtension.issuanceModule();
      expect(actualDebtIssuanceModule).to.eq(subjectDebtIssuanceModule);
    });

    it("should set the correct operator fee split", async () => {
      const feeExtension = await subject();

      const actualOperatorFeeSplit = await feeExtension.operatorFeeSplit();
      expect(actualOperatorFeeSplit).to.eq(subjectOperatorFeeSplit);
    });

    it("should set the correct operator fee recipient", async () => {
      const feeExtension = await subject();

      const actualOperatorFeeRecipient = await feeExtension.operatorFeeRecipient();
      expect(actualOperatorFeeRecipient).to.eq(subjectOperatorFeeRecipient);
    });
  });

  context("when fee extension is deployed and system fully set up", async () => {
    const operatorSplit: BigNumber = ether(.7);

    beforeEach(async () => {
      feeExtension = await deployer.extensions.deployFeeSplitExtension(
        baseManagerV2.address,
        setV2Setup.streamingFeeModule.address,
        setV2Setup.debtIssuanceModule.address,
        operatorSplit,
        operatorFeeRecipient.address
      );

      await baseManagerV2.connect(operator.wallet).addExtension(feeExtension.address);

      // Transfer ownership to BaseManager
      await setToken.setManager(baseManagerV2.address);

      // Protect StreamingFeeModule
      await baseManagerV2
        .connect(operator.wallet)
        .protectModule(setV2Setup.streamingFeeModule.address, [feeExtension.address]);

      // Set extension as fee recipient
      await feeExtension.connect(operator.wallet).updateFeeRecipient(feeExtension.address);
      await feeExtension.connect(methodologist.wallet).updateFeeRecipient(feeExtension.address);
    });

    describe("#accrueFeesAndDistribute", async () => {
      let mintedTokens: BigNumber;
      const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS;

      beforeEach(async () => {
        mintedTokens = ether(2);
        await setV2Setup.dai.approve(setV2Setup.debtIssuanceModule.address, ether(3));
        await setV2Setup.debtIssuanceModule.issue(setToken.address, mintedTokens, owner.address);

        await increaseTimeAsync(timeFastForward);
      });

      async function subject(): Promise<ContractTransaction> {
        return await feeExtension.accrueFeesAndDistribute();
      }

      it("should send correct amount of fees to operator fee recipient and methodologist", async () => {
        const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        const totalSupply = await setToken.totalSupply();

        const txnTimestamp = await getTransactionTimestamp(subject());

        const expectedFeeInflation = await getStreamingFee(
          setV2Setup.streamingFeeModule,
          setToken.address,
          feeState.lastStreamingFeeTimestamp,
          txnTimestamp
        );

        const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

        const expectedMintRedeemFees = preciseMul(mintedTokens, ether(.01));
        const expectedOperatorTake = preciseMul(feeInflation.add(expectedMintRedeemFees), operatorSplit);
        const expectedMethodologistTake = feeInflation.add(expectedMintRedeemFees).sub(expectedOperatorTake);

        const operatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
        const methodologistBalance = await setToken.balanceOf(methodologist.address);

        expect(operatorFeeRecipientBalance).to.eq(expectedOperatorTake);
        expect(methodologistBalance).to.eq(expectedMethodologistTake);
      });

      it("should emit a FeesDistributed event", async () => {
        await expect(subject()).to.emit(feeExtension, "FeesDistributed");
      });

      describe("when methodologist fees are 0", async () => {
        beforeEach(async () => {
          await feeExtension.connect(operator.wallet).updateFeeSplit(ether(1));
          await feeExtension.connect(methodologist.wallet).updateFeeSplit(ether(1));
        });

        it("should not send fees to methodologist", async () => {
          const preMethodologistBalance = await setToken.balanceOf(methodologist.address);

          await subject();

          const postMethodologistBalance = await setToken.balanceOf(methodologist.address);
          expect(postMethodologistBalance.sub(preMethodologistBalance)).to.eq(ZERO);
        });
      });

      describe("when operator fees are 0", async () => {
        beforeEach(async () => {
          await feeExtension.connect(operator.wallet).updateFeeSplit(ZERO);
          await feeExtension.connect(methodologist.wallet).updateFeeSplit(ZERO);
        });

        it("should not send fees to operator fee recipient", async () => {
          const preOperatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);

          await subject();

          const postOperatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
          expect(postOperatorFeeRecipientBalance.sub(preOperatorFeeRecipientBalance)).to.eq(ZERO);
        });
      });

      describe("when extension has fees accrued, is removed and no longer the feeRecipient", () => {
        let txnTimestamp: BigNumber;
        let feeState: any;
        let expectedFeeInflation: BigNumber;
        let totalSupply: BigNumber;

        beforeEach(async () => {
          feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
          totalSupply = await setToken.totalSupply();

          // Accrue fees to extension by StreamingFeeModule by direct call
          txnTimestamp = await getTransactionTimestamp(
            setV2Setup.streamingFeeModule.accrueFee(setToken.address)
          );

          expectedFeeInflation = await getStreamingFee(
            setV2Setup.streamingFeeModule,
            setToken.address,
            feeState.lastStreamingFeeTimestamp,
            txnTimestamp
          );

          // Change fee recipient to baseManagerV2;
          await feeExtension.connect(operator.wallet).updateFeeRecipient(baseManagerV2.address);
          await feeExtension.connect(methodologist.wallet).updateFeeRecipient(baseManagerV2.address);

          // Revoke extension authorization
          await baseManagerV2.connect(operator.wallet).revokeExtensionAuthorization(
            setV2Setup.streamingFeeModule.address,
            feeExtension.address
          );

          await baseManagerV2.connect(methodologist.wallet).revokeExtensionAuthorization(
            setV2Setup.streamingFeeModule.address,
            feeExtension.address
          );

          // Remove extension
          await baseManagerV2.connect(operator.wallet).removeExtension(feeExtension.address);
        });

        it("should send residual fees to operator fee recipient and methodologist", async () => {
          await subject();

          const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

          const expectedMintRedeemFees = preciseMul(mintedTokens, ether(.01));
          const expectedOperatorTake = preciseMul(feeInflation.add(expectedMintRedeemFees), operatorSplit);
          const expectedMethodologistTake = feeInflation.add(expectedMintRedeemFees).sub(expectedOperatorTake);

          const operatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
          const methodologistBalance = await setToken.balanceOf(methodologist.address);

          expect(operatorFeeRecipientBalance).to.eq(expectedOperatorTake);
          expect(methodologistBalance).to.eq(expectedMethodologistTake);
        });
      });
    });

    describe("#initializeIssuanceModule", () => {
      let subjectSetToken: Address;
      let subjectExtension: FeeSplitExtension;
      let subjectIssuanceModule: DebtIssuanceModule;
      let subjectStreamingFeeModule: StreamingFeeModule;
      let subjectManager: Address;
      let subjectOperatorFeeSplit: BigNumber;
      let subjectOperatorFeeRecipient: Address;
      let subjectMaxManagerFee: BigNumber;
      let subjectManagerIssueFee: BigNumber;
      let subjectManagerRedeemFee: BigNumber;
      let subjectManagerIssuanceHook: Address;

      beforeEach( async () => {
        subjectSetToken = setToken.address;
        subjectManager = baseManagerV2.address;
        subjectStreamingFeeModule = setV2Setup.streamingFeeModule;
        subjectOperatorFeeSplit = ether(.7);
        subjectOperatorFeeRecipient = operator.address;
        subjectMaxManagerFee = ether(.1);
        subjectManagerIssueFee = ether(.01);
        subjectManagerRedeemFee = ether(.005);
        subjectManagerIssuanceHook = ADDRESS_ZERO;

        // Protect current issuance Module
        await baseManagerV2.connect(operator.wallet).protectModule(setV2Setup.debtIssuanceModule.address, []);

        // Deploy new issuance module
        subjectIssuanceModule = await deployer.setV2.deployDebtIssuanceModule(setV2Setup.controller.address);
        await setV2Setup.controller.addModule(subjectIssuanceModule.address);

        // Deploy new issuance extension
        subjectExtension = await deployer.extensions.deployFeeSplitExtension(
          subjectManager,
          subjectStreamingFeeModule.address,
          subjectIssuanceModule.address,
          subjectOperatorFeeSplit,
          subjectOperatorFeeRecipient,
        );

        // Replace module and extension
        await baseManagerV2.connect(operator.wallet).replaceProtectedModule(
          setV2Setup.debtIssuanceModule.address,
          subjectIssuanceModule.address,
          [subjectExtension.address]
        );

        await baseManagerV2.connect(methodologist.wallet).replaceProtectedModule(
          setV2Setup.debtIssuanceModule.address,
          subjectIssuanceModule.address,
          [subjectExtension.address]
        );

        // Authorize new extension for StreamingFeeModule too..
        await baseManagerV2.connect(operator.wallet).authorizeExtension(
          subjectStreamingFeeModule.address,
          subjectExtension.address
        );

        await baseManagerV2.connect(methodologist.wallet).authorizeExtension(
          subjectStreamingFeeModule.address,
          subjectExtension.address
        );
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
         return await subjectExtension.connect(caller.wallet).initializeIssuanceModule(
           subjectMaxManagerFee,
           subjectManagerIssueFee,
           subjectManagerRedeemFee,
           subjectExtension.address,
           subjectManagerIssuanceHook
         );
      }

      context("when both parties call the method", async () => {
        it("should initialize the debt issuance module", async () => {
          const initialFeeRecipient = (
            await subjectIssuanceModule.issuanceSettings(subjectSetToken)
          ).feeRecipient;

          await subject(operator);
          await subject(methodologist);

          const finalFeeRecipient = (
            await subjectIssuanceModule.issuanceSettings(subjectSetToken)
          ).feeRecipient;

          expect(initialFeeRecipient).to.equal(ADDRESS_ZERO);
          expect(finalFeeRecipient).to.equal(subjectExtension.address);
        });

        it("should enable calls on the protected module", async () => {
          const newFeeRecipient = baseManagerV2.address;

          await subject(operator);
          await subject(methodologist);

          // Reset fee recipient
          await subjectExtension.connect(operator.wallet).updateFeeRecipient(newFeeRecipient);
          await subjectExtension.connect(methodologist.wallet).updateFeeRecipient(newFeeRecipient);

          const receivedFeeRecipient = (
            await subjectIssuanceModule.issuanceSettings(subjectSetToken)
          ).feeRecipient;

          expect(receivedFeeRecipient).to.equal(newFeeRecipient);
        });
      });

      context("when a single mutual upgrade party has called the method", async () => {
        afterEach(async () => await subject(methodologist));

        it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
          const txHash = await subject(operator);

          const expectedHash = solidityKeccak256(
            ["bytes", "address"],
            [txHash.data, operator.address]
          );

          const isLogged = await subjectExtension.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        it("should revert", async () => {
          await expect(subject(await getRandomAccount())).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#initializeStreamingFeeModule", () => {
      let subjectSetToken: Address;
      let subjectFeeSettings: any;
      let subjectExtension: FeeSplitExtension;
      let subjectIssuanceModule: DebtIssuanceModule;
      let subjectFeeModule: StreamingFeeModule;
      let subjectManager: Address;
      let subjectOperatorFeeSplit: BigNumber;
      let subjectOperatorFeeRecipient: Address;

      beforeEach( async () => {
        subjectSetToken = setToken.address;
        subjectIssuanceModule = setV2Setup.debtIssuanceModule;
        subjectManager = baseManagerV2.address;
        subjectOperatorFeeSplit = ether(.7);
        subjectOperatorFeeRecipient = operator.address;

        // Deploy new fee module
        subjectFeeModule = await deployer.setV2.deployStreamingFeeModule(setV2Setup.controller.address);
        await setV2Setup.controller.addModule(subjectFeeModule.address);

        // Deploy new fee extension
        subjectExtension = await deployer.extensions.deployFeeSplitExtension(
          subjectManager,
          subjectFeeModule.address,
          subjectIssuanceModule.address,
          subjectOperatorFeeSplit,
          subjectOperatorFeeRecipient,
        );

        // Replace module and extension
        await baseManagerV2.connect(operator.wallet).replaceProtectedModule(
          setV2Setup.streamingFeeModule.address,
          subjectFeeModule.address,
          [subjectExtension.address]
        );

        await baseManagerV2.connect(methodologist.wallet).replaceProtectedModule(
          setV2Setup.streamingFeeModule.address,
          subjectFeeModule.address,
          [subjectExtension.address]
        );

        subjectFeeSettings = {
          feeRecipient: subjectExtension.address,
          maxStreamingFeePercentage: ether(.01),
          streamingFeePercentage: ether(.01),
          lastStreamingFeeTimestamp: ZERO,
        };
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
         return await subjectExtension
           .connect(caller.wallet)
           .initializeStreamingFeeModule(subjectFeeSettings);
      }

      context("when both parties call the method", async () => {
        it("should initialize the streaming fee module", async () => {
          const initialFeeRecipient = (await subjectFeeModule.feeStates(subjectSetToken)).feeRecipient;

          await subject(operator);
          await subject(methodologist);

          const finalFeeRecipient = (await subjectFeeModule.feeStates(subjectSetToken)).feeRecipient;

          expect(initialFeeRecipient).to.equal(ADDRESS_ZERO);
          expect(finalFeeRecipient).to.equal(subjectExtension.address);
        });

        it("should enable calls on the protected module", async () => {
          const newFeeRecipient = baseManagerV2.address;

          await subject(operator);
          await subject(methodologist);

          // Reset fee recipient
          await subjectExtension.connect(operator.wallet).updateFeeRecipient(newFeeRecipient);
          await subjectExtension.connect(methodologist.wallet).updateFeeRecipient(newFeeRecipient);

          const receivedFeeRecipient = (await subjectFeeModule.feeStates(subjectSetToken)).feeRecipient;

          expect(receivedFeeRecipient).to.equal(newFeeRecipient);
        });
      });

      context("when a single mutual upgrade party has called the method", async () => {
        afterEach(async () => await subject(methodologist));

        it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
          const txHash = await subject(operator);

          const expectedHash = solidityKeccak256(
            ["bytes", "address"],
            [txHash.data, operator.address]
          );

          const isLogged = await subjectExtension.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        it("should revert", async () => {
          await expect(subject(await getRandomAccount())).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateStreamingFee", async () => {
      let mintedTokens: BigNumber;
      const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS;

      let subjectNewFee: BigNumber;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      beforeEach(async () => {
        mintedTokens = ether(2);
        await setV2Setup.dai.approve(setV2Setup.debtIssuanceModule.address, ether(3));
        await setV2Setup.debtIssuanceModule.issue(setToken.address, mintedTokens, owner.address);

        await increaseTimeAsync(timeFastForward);

        subjectNewFee = ether(.01);
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateStreamingFee(subjectNewFee);
      }

      context("when no timelock period has been set", async () => {

        context("when a single mutual upgrade party has called the method", () => {
          it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
            const txHash = await subject(subjectOperatorCaller);

            const expectedHash = solidityKeccak256(
              ["bytes", "address"],
              [txHash.data, subjectOperatorCaller.address]
            );

            const isLogged = await feeExtension.mutualUpgrades(expectedHash);

            expect(isLogged).to.be.true;
          });
        });

        context("when both upgrade parties have called the method", () => {
          it("should update the streaming fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
            expect(feeState.streamingFeePercentage).to.eq(subjectNewFee);
          });

          it("should send correct amount of fees to the fee extension", async () => {
            const preExtensionBalance = await setToken.balanceOf(feeExtension.address);
            const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
            const totalSupply = await setToken.totalSupply();

            await subject(subjectOperatorCaller);
            const txnTimestamp = await getTransactionTimestamp(subject(subjectMethodologistCaller));

            const expectedFeeInflation = await getStreamingFee(
              setV2Setup.streamingFeeModule,
              setToken.address,
              feeState.lastStreamingFeeTimestamp,
              txnTimestamp,
              ether(.02)
            );

            const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

            const postExtensionBalance = await setToken.balanceOf(feeExtension.address);

            expect(postExtensionBalance.sub(preExtensionBalance)).to.eq(feeInflation);
          });
        });
      });

      context("when 1 day timelock period has been set", async () => {
        beforeEach(async () => {
          await feeExtension.connect(owner.wallet).setTimeLockPeriod(ONE_DAY_IN_SECONDS);
        });

        it("sets the upgradeHash", async () => {
          await subject(subjectOperatorCaller);
          await subject(subjectMethodologistCaller);
          const timestamp = await getLastBlockTimestamp();
          const calldata = feeExtension.interface.encodeFunctionData("updateStreamingFee", [subjectNewFee]);
          const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
          const actualTimestamp = await feeExtension.timeLockedUpgrades(upgradeHash);
          expect(actualTimestamp).to.eq(timestamp);
        });

        context("when 1 day timelock has elapsed", async () => {
          beforeEach(async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);
            await increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1));
          });

          it("should update the streaming fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);

            expect(feeState.streamingFeePercentage).to.eq(subjectNewFee);
          });

          it("should send correct amount of fees to the fee extension", async () => {
            const preExtensionBalance = await setToken.balanceOf(feeExtension.address);
            const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
            const totalSupply = await setToken.totalSupply();

            await subject(subjectOperatorCaller);
            const txnTimestamp = await getTransactionTimestamp(subject(subjectMethodologistCaller));

            const expectedFeeInflation = await getStreamingFee(
              setV2Setup.streamingFeeModule,
              setToken.address,
              feeState.lastStreamingFeeTimestamp,
              txnTimestamp,
              ether(.02)
            );

            const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

            const postExtensionBalance = await setToken.balanceOf(feeExtension.address);

            expect(postExtensionBalance.sub(preExtensionBalance)).to.eq(feeInflation);
          });
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateIssueFee", async () => {
      let subjectNewFee: BigNumber;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      beforeEach(async () => {
        subjectNewFee = ether(.02);
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateIssueFee(subjectNewFee);
      }

      context("when no timelock period has been set", async () => {
        context("when a single mutual upgrade party has called the method", () => {
          it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
            const txHash = await subject(subjectOperatorCaller);

            const expectedHash = solidityKeccak256(
              ["bytes", "address"],
              [txHash.data, subjectOperatorCaller.address]
            );

            const isLogged = await feeExtension.mutualUpgrades(expectedHash);

            expect(isLogged).to.be.true;
          });
        });

        context("when both upgrade parties have called the method", () => {
          it("should update the issue fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const issueState: any = await setV2Setup.debtIssuanceModule.issuanceSettings(setToken.address);

            expect(issueState.managerIssueFee).to.eq(subjectNewFee);
          });
        });
      });

      context("when 1 day timelock period has been set", async () => {
        beforeEach(async () => {
          await feeExtension.connect(owner.wallet).setTimeLockPeriod(ONE_DAY_IN_SECONDS);
        });

        it("sets the upgradeHash", async () => {
          await subject(subjectOperatorCaller);
          await subject(subjectMethodologistCaller);

          const timestamp = await getLastBlockTimestamp();
          const calldata = feeExtension.interface.encodeFunctionData("updateIssueFee", [subjectNewFee]);
          const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
          const actualTimestamp = await feeExtension.timeLockedUpgrades(upgradeHash);
          expect(actualTimestamp).to.eq(timestamp);
        });

        context("when 1 day timelock has elapsed", async () => {
          beforeEach(async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);
            await increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1));
          });

          it("sets the new issue fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const issueState: any = await setV2Setup.debtIssuanceModule.issuanceSettings(setToken.address);
            expect(issueState.managerIssueFee).to.eq(subjectNewFee);
          });

          it("sets the upgradeHash to 0", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const calldata = feeExtension.interface.encodeFunctionData("updateIssueFee", [subjectNewFee]);
            const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
            const actualTimestamp = await feeExtension.timeLockedUpgrades(upgradeHash);
            expect(actualTimestamp).to.eq(ZERO);
          });
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateRedeemFee", async () => {
      let subjectNewFee: BigNumber;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      beforeEach(async () => {
        subjectNewFee = ether(.02);
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateRedeemFee(subjectNewFee);
      }

      context("when no timelock period has been set", () => {
        context("when a single mutual upgrade party has called the method", () => {
          it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
            const txHash = await subject(subjectOperatorCaller);

            const expectedHash = solidityKeccak256(
              ["bytes", "address"],
              [txHash.data, subjectOperatorCaller.address]
            );

            const isLogged = await feeExtension.mutualUpgrades(expectedHash);

            expect(isLogged).to.be.true;
          });
        });

        context("when both upgrade parties have called the method", () => {
          it("should update the redeem fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const issuanceState: any = await setV2Setup.debtIssuanceModule.issuanceSettings(setToken.address);

            expect(issuanceState.managerRedeemFee).to.eq(subjectNewFee);
          });
        });
      });

      context("when 1 day timelock period has been set", async () => {
        beforeEach(async () => {
          await feeExtension.connect(owner.wallet).setTimeLockPeriod(ONE_DAY_IN_SECONDS);
        });

        it("sets the upgradeHash", async () => {
          await subject(subjectOperatorCaller);
          await subject(subjectMethodologistCaller);

          const timestamp = await getLastBlockTimestamp();
          const calldata = feeExtension.interface.encodeFunctionData("updateRedeemFee", [subjectNewFee]);
          const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
          const actualTimestamp = await feeExtension.timeLockedUpgrades(upgradeHash);
          expect(actualTimestamp).to.eq(timestamp);
        });

        context("when 1 day timelock has elapsed", async () => {
          beforeEach(async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);
            await increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1));
          });

          it("sets the new redeem fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const issuanceState: any = await setV2Setup.debtIssuanceModule.issuanceSettings(setToken.address);
            expect(issuanceState.managerRedeemFee).to.eq(subjectNewFee);
          });

          it("sets the upgradeHash to 0", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const calldata = feeExtension.interface.encodeFunctionData("updateRedeemFee", [subjectNewFee]);
            const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
            const actualTimestamp = await feeExtension.timeLockedUpgrades(upgradeHash);
            expect(actualTimestamp).to.eq(ZERO);
          });
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateFeeRecipient", async () => {
      let subjectNewFeeRecipient: Address;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      beforeEach(async () => {
        subjectNewFeeRecipient = owner.address;
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateFeeRecipient(subjectNewFeeRecipient);
      }

      context("when a single mutual upgrade party has called the method", () => {
        it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
          const txHash = await subject(subjectOperatorCaller);

          const expectedHash = solidityKeccak256(
            ["bytes", "address"],
            [txHash.data, subjectOperatorCaller.address]
          );

          const isLogged = await feeExtension.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });
      });

      context("when operator and methodologist both execute update", () => {
        it("sets the new fee recipients", async () => {
          await subject(subjectOperatorCaller);
          await subject(subjectMethodologistCaller);

          const streamingFeeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
          const issuanceFeeState = await setV2Setup.debtIssuanceModule.issuanceSettings(setToken.address);

          expect(streamingFeeState.feeRecipient).to.eq(subjectNewFeeRecipient);
          expect(issuanceFeeState.feeRecipient).to.eq(subjectNewFeeRecipient);
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateFeeSplit", async () => {
      let subjectNewFeeSplit: BigNumber;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      const mintedTokens: BigNumber = ether(2);
      const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS;

      beforeEach(async () => {
        await setV2Setup.dai.approve(setV2Setup.debtIssuanceModule.address, ether(3));
        await setV2Setup.debtIssuanceModule.issue(setToken.address, mintedTokens, owner.address);

        await increaseTimeAsync(timeFastForward);

        subjectNewFeeSplit = ether(.5);
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateFeeSplit(subjectNewFeeSplit);
      }

      context("when operator and methodologist both execute update", () => {
        it("should accrue fees and send correct amount to operator fee recipient and methodologist", async () => {
          const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
          const totalSupply = await setToken.totalSupply();

          await subject(subjectOperatorCaller);
          const txnTimestamp = await getTransactionTimestamp(await subject(subjectMethodologistCaller));

          const expectedFeeInflation = await getStreamingFee(
            setV2Setup.streamingFeeModule,
            setToken.address,
            feeState.lastStreamingFeeTimestamp,
            txnTimestamp
          );

          const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

          const expectedMintRedeemFees = preciseMul(mintedTokens, ether(.01));
          const expectedOperatorTake = preciseMul(feeInflation.add(expectedMintRedeemFees), operatorSplit);
          const expectedMethodologistTake = feeInflation.add(expectedMintRedeemFees).sub(expectedOperatorTake);

          const operatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
          const methodologistBalance = await setToken.balanceOf(methodologist.address);

          expect(operatorFeeRecipientBalance).to.eq(expectedOperatorTake);
          expect(methodologistBalance).to.eq(expectedMethodologistTake);
        });

        it("sets the new fee split", async () => {
          await subject(subjectOperatorCaller);
          await subject(subjectMethodologistCaller);

          const actualFeeSplit = await feeExtension.operatorFeeSplit();

          expect(actualFeeSplit).to.eq(subjectNewFeeSplit);
        });

        describe("when fee splits is >100%", async () => {
          beforeEach(async () => {
            subjectNewFeeSplit = ether(1.1);
          });

          it("should revert", async () => {
            await subject(subjectOperatorCaller);
            await expect(subject(subjectMethodologistCaller)).to.be.revertedWith("Fee must be less than 100%");
          });
        });
      });

      context("when a single mutual upgrade party has called the method", async () => {
        afterEach(async () => await subject(subjectMethodologistCaller));

        it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
          const txHash = await subject(subjectOperatorCaller);

          const expectedHash = solidityKeccak256(
            ["bytes", "address"],
            [txHash.data, subjectOperatorCaller.address]
          );

          const isLogged = await feeExtension.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateOperatorFeeRecipient", async () => {
      let subjectCaller: Account;
      let subjectOperatorFeeRecipient: Address;

      beforeEach(async () => {
        subjectCaller = operator;
        subjectOperatorFeeRecipient = (await getRandomAccount()).address;
      });

      async function subject(): Promise<ContractTransaction> {
        return await feeExtension
          .connect(subjectCaller.wallet)
          .updateOperatorFeeRecipient(subjectOperatorFeeRecipient);
      }

      it("sets the new operator fee recipient", async () => {
        await subject();

        const newOperatorFeeRecipient = await feeExtension.operatorFeeRecipient();
        expect(newOperatorFeeRecipient).to.eq(subjectOperatorFeeRecipient);
      });

      describe("when the new operator fee recipient is address zero", async () => {
        beforeEach(async () => {
          subjectOperatorFeeRecipient = ADDRESS_ZERO;
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Zero address not valid");
        });
      });

      describe("when the caller is not the operator", async () => {
        beforeEach(async () => {
          subjectCaller = methodologist;
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Must be operator");
        });
      });
    });
  });
});
Example #11
Source File: streamingFeeSplitExtension.spec.ts    From index-coop-smart-contracts with Apache License 2.0 4 votes vote down vote up
describe("StreamingFeeSplitExtension", () => {
  let owner: Account;
  let methodologist: Account;
  let operator: Account;
  let operatorFeeRecipient: Account;
  let setV2Setup: SetFixture;

  let deployer: DeployHelper;
  let setToken: SetToken;

  let baseManagerV2: BaseManagerV2;
  let feeExtension: StreamingFeeSplitExtension;

  before(async () => {
    [
      owner,
      methodologist,
      operator,
      operatorFeeRecipient,
    ] = await getAccounts();

    deployer = new DeployHelper(owner.wallet);

    setV2Setup = getSetFixture(owner.address);
    await setV2Setup.initialize();

    setToken = await setV2Setup.createSetToken(
      [setV2Setup.dai.address],
      [ether(1)],
      [setV2Setup.issuanceModule.address, setV2Setup.streamingFeeModule.address]
    );

    // Deploy BaseManager
    baseManagerV2 = await deployer.manager.deployBaseManagerV2(
      setToken.address,
      operator.address,
      methodologist.address
    );
    await baseManagerV2.connect(methodologist.wallet).authorizeInitialization();

    const feeRecipient = baseManagerV2.address;
    const maxStreamingFeePercentage = ether(.1);
    const streamingFeePercentage = ether(.02);
    const streamingFeeSettings = {
      feeRecipient,
      maxStreamingFeePercentage,
      streamingFeePercentage,
      lastStreamingFeeTimestamp: ZERO,
    };
    await setV2Setup.streamingFeeModule.initialize(setToken.address, streamingFeeSettings);

    await setV2Setup.issuanceModule.initialize(
      setToken.address,
      ADDRESS_ZERO
    );
  });

  addSnapshotBeforeRestoreAfterEach();

  describe("#constructor", async () => {
    let subjectManager: Address;
    let subjectStreamingFeeModule: Address;
    let subjectOperatorFeeSplit: BigNumber;
    let subjectOperatorFeeRecipient: Address;

    beforeEach(async () => {
      subjectManager = baseManagerV2.address;
      subjectStreamingFeeModule = setV2Setup.streamingFeeModule.address;
      subjectOperatorFeeSplit = ether(.7);
      subjectOperatorFeeRecipient = operatorFeeRecipient.address;
    });

    async function subject(): Promise<StreamingFeeSplitExtension> {
      return await deployer.extensions.deployStreamingFeeSplitExtension(
        subjectManager,
        subjectStreamingFeeModule,
        subjectOperatorFeeSplit,
        subjectOperatorFeeRecipient,
      );
    }

    it("should set the correct SetToken address", async () => {
      const feeExtension = await subject();

      const actualToken = await feeExtension.setToken();
      expect(actualToken).to.eq(setToken.address);
    });

    it("should set the correct manager address", async () => {
      const feeExtension = await subject();

      const actualManager = await feeExtension.manager();
      expect(actualManager).to.eq(baseManagerV2.address);
    });

    it("should set the correct streaming fee module address", async () => {
      const feeExtension = await subject();

      const actualStreamingFeeModule = await feeExtension.streamingFeeModule();
      expect(actualStreamingFeeModule).to.eq(subjectStreamingFeeModule);
    });

    it("should set the correct operator fee split", async () => {
      const feeExtension = await subject();

      const actualOperatorFeeSplit = await feeExtension.operatorFeeSplit();
      expect(actualOperatorFeeSplit).to.eq(subjectOperatorFeeSplit);
    });

    it("should set the correct operator fee recipient", async () => {
      const feeExtension = await subject();

      const actualOperatorFeeRecipient = await feeExtension.operatorFeeRecipient();
      expect(actualOperatorFeeRecipient).to.eq(subjectOperatorFeeRecipient);
    });
  });

  context("when fee extension is deployed and system fully set up", async () => {
    const operatorSplit: BigNumber = ether(.7);

    beforeEach(async () => {
      feeExtension = await deployer.extensions.deployStreamingFeeSplitExtension(
        baseManagerV2.address,
        setV2Setup.streamingFeeModule.address,
        operatorSplit,
        operatorFeeRecipient.address
      );

      await baseManagerV2.connect(operator.wallet).addExtension(feeExtension.address);

      // Transfer ownership to BaseManager
      await setToken.setManager(baseManagerV2.address);

      // Protect StreamingFeeModule
      await baseManagerV2
        .connect(operator.wallet)
        .protectModule(setV2Setup.streamingFeeModule.address, [feeExtension.address]);

      // Set extension as fee recipient
      await feeExtension.connect(operator.wallet).updateFeeRecipient(feeExtension.address);
      await feeExtension.connect(methodologist.wallet).updateFeeRecipient(feeExtension.address);
    });

    describe("#accrueFeesAndDistribute", async () => {
      let mintedTokens: BigNumber;
      const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS;

      beforeEach(async () => {
        mintedTokens = ether(2);
        await setV2Setup.dai.approve(setV2Setup.issuanceModule.address, ether(3));
        await setV2Setup.issuanceModule.issue(setToken.address, mintedTokens, owner.address);

        await increaseTimeAsync(timeFastForward);
      });

      async function subject(): Promise<ContractTransaction> {
        return await feeExtension.accrueFeesAndDistribute();
      }

      it("should send correct amount of fees to operator fee recipient and methodologist", async () => {
        const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        const totalSupply = await setToken.totalSupply();

        const txnTimestamp = await getTransactionTimestamp(subject());

        const expectedFeeInflation = await getStreamingFee(
          setV2Setup.streamingFeeModule,
          setToken.address,
          feeState.lastStreamingFeeTimestamp,
          txnTimestamp
        );

        const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

        const expectedOperatorTake = preciseMul(feeInflation, operatorSplit);
        const expectedMethodologistTake = feeInflation.sub(expectedOperatorTake);

        const operatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
        const methodologistBalance = await setToken.balanceOf(methodologist.address);

        expect(operatorFeeRecipientBalance).to.eq(expectedOperatorTake);
        expect(methodologistBalance).to.eq(expectedMethodologistTake);
      });

      it("should emit a FeesDistributed event", async () => {
        await expect(subject()).to.emit(feeExtension, "FeesDistributed");
      });

      describe("when methodologist fees are 0", async () => {
        beforeEach(async () => {
          await feeExtension.connect(operator.wallet).updateFeeSplit(ether(1));
          await feeExtension.connect(methodologist.wallet).updateFeeSplit(ether(1));
        });

        it("should not send fees to methodologist", async () => {
          const preMethodologistBalance = await setToken.balanceOf(methodologist.address);

          await subject();

          const postMethodologistBalance = await setToken.balanceOf(methodologist.address);
          expect(postMethodologistBalance.sub(preMethodologistBalance)).to.eq(ZERO);
        });
      });

      describe("when operator fees are 0", async () => {
        beforeEach(async () => {
          await feeExtension.connect(operator.wallet).updateFeeSplit(ZERO);
          await feeExtension.connect(methodologist.wallet).updateFeeSplit(ZERO);
        });

        it("should not send fees to operator fee recipient", async () => {
          const preOperatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);

          await subject();

          const postOperatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
          expect(postOperatorFeeRecipientBalance.sub(preOperatorFeeRecipientBalance)).to.eq(ZERO);
        });
      });

      describe("when extension has fees accrued, is removed and no longer the feeRecipient", () => {
        let txnTimestamp: BigNumber;
        let feeState: any;
        let expectedFeeInflation: BigNumber;
        let totalSupply: BigNumber;

        beforeEach(async () => {
          feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
          totalSupply = await setToken.totalSupply();

          // Accrue fees to extension by StreamingFeeModule by direct call
          txnTimestamp = await getTransactionTimestamp(
            setV2Setup.streamingFeeModule.accrueFee(setToken.address)
          );

          expectedFeeInflation = await getStreamingFee(
            setV2Setup.streamingFeeModule,
            setToken.address,
            feeState.lastStreamingFeeTimestamp,
            txnTimestamp
          );

          // Change fee recipient to baseManagerV2;
          await feeExtension.connect(operator.wallet).updateFeeRecipient(baseManagerV2.address);
          await feeExtension.connect(methodologist.wallet).updateFeeRecipient(baseManagerV2.address);

          // Revoke extension authorization
          await baseManagerV2.connect(operator.wallet).revokeExtensionAuthorization(
            setV2Setup.streamingFeeModule.address,
            feeExtension.address
          );

          await baseManagerV2.connect(methodologist.wallet).revokeExtensionAuthorization(
            setV2Setup.streamingFeeModule.address,
            feeExtension.address
          );

          // Remove extension
          await baseManagerV2.connect(operator.wallet).removeExtension(feeExtension.address);
        });

        it("should send residual fees to operator fee recipient and methodologist", async () => {
          await subject();

          const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

          const expectedOperatorTake = preciseMul(feeInflation, operatorSplit);
          const expectedMethodologistTake = feeInflation.sub(expectedOperatorTake);

          const operatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
          const methodologistBalance = await setToken.balanceOf(methodologist.address);

          expect(operatorFeeRecipientBalance).to.eq(expectedOperatorTake);
          expect(methodologistBalance).to.eq(expectedMethodologistTake);
          });
      });
    });

    describe("#initializeModule", () => {
      let subjectSetToken: Address;
      let subjectFeeSettings: any;
      let subjectExtension: StreamingFeeSplitExtension;
      let subjectModule: StreamingFeeModule;
      let subjectManager: Address;
      let subjectOperatorFeeSplit: BigNumber;
      let subjectOperatorFeeRecipient: Address;

      beforeEach( async () => {
        subjectSetToken = setToken.address;
        subjectManager = baseManagerV2.address;
        subjectOperatorFeeSplit = ether(.7);
        subjectOperatorFeeRecipient = operator.address;

        // Deploy new fee module
        subjectModule = await deployer.setV2.deployStreamingFeeModule(setV2Setup.controller.address);
        await setV2Setup.controller.addModule(subjectModule.address);

        // Deploy new fee extension
        subjectExtension = await deployer.extensions.deployStreamingFeeSplitExtension(
          subjectManager,
          subjectModule.address,
          subjectOperatorFeeSplit,
          subjectOperatorFeeRecipient,
        );

        // Replace module and extension
        await baseManagerV2.connect(operator.wallet).replaceProtectedModule(
          setV2Setup.streamingFeeModule.address,
          subjectModule.address,
          [subjectExtension.address]
        );

        await baseManagerV2.connect(methodologist.wallet).replaceProtectedModule(
          setV2Setup.streamingFeeModule.address,
          subjectModule.address,
          [subjectExtension.address]
        );

        subjectFeeSettings = {
          feeRecipient: subjectExtension.address,
          maxStreamingFeePercentage: ether(.01),
          streamingFeePercentage: ether(.01),
          lastStreamingFeeTimestamp: ZERO,
        };
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
         return await subjectExtension.connect(caller.wallet).initializeModule(subjectFeeSettings);
      }

      context("when both parties call the method", async () => {
        it("should initialize the streaming fee module", async () => {
          const initialFeeRecipient = (await subjectModule.feeStates(subjectSetToken)).feeRecipient;

          await subject(operator);
          await subject(methodologist);

          const finalFeeRecipient = (await subjectModule.feeStates(subjectSetToken)).feeRecipient;

          expect(initialFeeRecipient).to.equal(ADDRESS_ZERO);
          expect(finalFeeRecipient).to.equal(subjectExtension.address);
        });

        it("should enable calls on the protected module", async () => {
          const newFeeRecipient = baseManagerV2.address;

          await subject(operator);
          await subject(methodologist);

          // Reset fee recipient
          await subjectExtension.connect(operator.wallet).updateFeeRecipient(newFeeRecipient);
          await subjectExtension.connect(methodologist.wallet).updateFeeRecipient(newFeeRecipient);

          const receivedFeeRecipient = (await subjectModule.feeStates(subjectSetToken)).feeRecipient;

          expect(receivedFeeRecipient).to.equal(newFeeRecipient);
        });
      });

      context("when a single mutual upgrade party has called the method", async () => {
        afterEach(async () => await subject(methodologist));

        it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
          const txHash = await subject(operator);

          const expectedHash = solidityKeccak256(
            ["bytes", "address"],
            [txHash.data, operator.address]
          );

          const isLogged = await subjectExtension.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        it("should revert", async () => {
          await expect(subject(await getRandomAccount())).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateStreamingFee", async () => {
      let mintedTokens: BigNumber;
      const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS;

      let subjectNewFee: BigNumber;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      beforeEach(async () => {
        mintedTokens = ether(2);
        await setV2Setup.dai.approve(setV2Setup.issuanceModule.address, ether(3));
        await setV2Setup.issuanceModule.issue(setToken.address, mintedTokens, owner.address);

        await increaseTimeAsync(timeFastForward);

        subjectNewFee = ether(.01);
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateStreamingFee(subjectNewFee);
      }

      context("when no timelock period has been set", async () => {

        context("when only one mutual upgrade party has called the method", () => {
          it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
            const txHash = await subject(subjectOperatorCaller);

            const expectedHash = solidityKeccak256(
              ["bytes", "address"],
              [txHash.data, subjectOperatorCaller.address]
            );

            const isLogged = await feeExtension.mutualUpgrades(expectedHash);

            expect(isLogged).to.be.true;
          });
        });

        context("when both upgrade parties have called the method", () => {

          it("should update the streaming fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);

            expect(feeState.streamingFeePercentage).to.eq(subjectNewFee);
          });

          it("should send correct amount of fees to the fee extension", async () => {
            const preExtensionBalance = await setToken.balanceOf(feeExtension.address);
            const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
            const totalSupply = await setToken.totalSupply();

            await subject(subjectOperatorCaller);
            const txnTimestamp = await getTransactionTimestamp(subject(subjectMethodologistCaller));

            const expectedFeeInflation = await getStreamingFee(
              setV2Setup.streamingFeeModule,
              setToken.address,
              feeState.lastStreamingFeeTimestamp,
              txnTimestamp,
              ether(.02)
            );

            const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

            const postExtensionBalance = await setToken.balanceOf(feeExtension.address);

            expect(postExtensionBalance.sub(preExtensionBalance)).to.eq(feeInflation);
          });
        });
      });

      context("when 1 day timelock period has been set", async () => {
        beforeEach(async () => {
          await feeExtension.connect(owner.wallet).setTimeLockPeriod(ONE_DAY_IN_SECONDS);
        });

        it("sets the upgradeHash", async () => {
          await subject(subjectOperatorCaller);
          await subject(subjectMethodologistCaller);

          const timestamp = await getLastBlockTimestamp();
          const calldata = feeExtension.interface.encodeFunctionData("updateStreamingFee", [subjectNewFee]);
          const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
          const actualTimestamp = await feeExtension.timeLockedUpgrades(upgradeHash);
          expect(actualTimestamp).to.eq(timestamp);
        });

        context("when 1 day timelock has elapsed", async () => {
          beforeEach(async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);
            await increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1));
          });

          it("should update the streaming fee", async () => {
            await subject(subjectOperatorCaller);
            await subject(subjectMethodologistCaller);

            const feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);

            expect(feeState.streamingFeePercentage).to.eq(subjectNewFee);
          });

          it("should send correct amount of fees to the fee extension", async () => {
            const preExtensionBalance = await setToken.balanceOf(feeExtension.address);
            const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
            const totalSupply = await setToken.totalSupply();

            await subject(subjectOperatorCaller);
            const txnTimestamp = await getTransactionTimestamp(subject(subjectMethodologistCaller));

            const expectedFeeInflation = await getStreamingFee(
              setV2Setup.streamingFeeModule,
              setToken.address,
              feeState.lastStreamingFeeTimestamp,
              txnTimestamp,
              ether(.02)
            );

            const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

            const postExtensionBalance = await setToken.balanceOf(feeExtension.address);

            expect(postExtensionBalance.sub(preExtensionBalance)).to.eq(feeInflation);
          });
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateFeeRecipient", async () => {
      let subjectNewFeeRecipient: Address;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      beforeEach(async () => {
        subjectNewFeeRecipient = owner.address;
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateFeeRecipient(subjectNewFeeRecipient);
      }

      it("sets the new fee recipient", async () => {
        await subject(subjectOperatorCaller);
        await subject(subjectMethodologistCaller);

        const streamingFeeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);

        expect(streamingFeeState.feeRecipient).to.eq(subjectNewFeeRecipient);
      });

      context("when a single mutual upgrade party has called the method", async () => {
        afterEach(async () => await subject(subjectMethodologistCaller));

        it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
          const txHash = await subject(subjectOperatorCaller);

          const expectedHash = solidityKeccak256(
            ["bytes", "address"],
            [txHash.data, subjectOperatorCaller.address]
          );

          const isLogged = await feeExtension.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateFeeSplit", async () => {
      let subjectNewFeeSplit: BigNumber;
      let subjectOperatorCaller: Account;
      let subjectMethodologistCaller: Account;

      const mintedTokens: BigNumber = ether(2);
      const timeFastForward: BigNumber = ONE_YEAR_IN_SECONDS;

      beforeEach(async () => {
        await setV2Setup.dai.approve(setV2Setup.issuanceModule.address, ether(3));
        await setV2Setup.issuanceModule.issue(setToken.address, mintedTokens, owner.address);

        await increaseTimeAsync(timeFastForward);

        subjectNewFeeSplit = ether(.5);
        subjectOperatorCaller = operator;
        subjectMethodologistCaller = methodologist;
      });

      async function subject(caller: Account): Promise<ContractTransaction> {
        return await feeExtension.connect(caller.wallet).updateFeeSplit(subjectNewFeeSplit);
      }

      it("should accrue fees and send correct amount to operator fee recipient and methodologist", async () => {
        const feeState: any = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        const totalSupply = await setToken.totalSupply();

        await subject(subjectOperatorCaller);
        const txnTimestamp = await getTransactionTimestamp(subject(subjectMethodologistCaller));

        const expectedFeeInflation = await getStreamingFee(
          setV2Setup.streamingFeeModule,
          setToken.address,
          feeState.lastStreamingFeeTimestamp,
          txnTimestamp
        );

        const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);

        const expectedOperatorTake = preciseMul(feeInflation, operatorSplit);
        const expectedMethodologistTake = feeInflation.sub(expectedOperatorTake);

        const operatorFeeRecipientBalance = await setToken.balanceOf(operatorFeeRecipient.address);
        const methodologistBalance = await setToken.balanceOf(methodologist.address);

        expect(operatorFeeRecipientBalance).to.eq(expectedOperatorTake);
        expect(methodologistBalance).to.eq(expectedMethodologistTake);
      });

      it("sets the new fee split", async () => {
        await subject(subjectOperatorCaller);
        await subject(subjectMethodologistCaller);

        const actualFeeSplit = await feeExtension.operatorFeeSplit();

        expect(actualFeeSplit).to.eq(subjectNewFeeSplit);
      });

      describe("when fee splits is >100%", async () => {
        beforeEach(async () => {
          subjectNewFeeSplit = ether(1.1);
        });

        it("should revert", async () => {
          await subject(subjectOperatorCaller);
          await expect(subject(subjectMethodologistCaller)).to.be.revertedWith("Fee must be less than 100%");
        });
      });

      context("when a single mutual upgrade party has called the method", async () => {
        it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
          const txHash = await subject(subjectOperatorCaller);

          const expectedHash = solidityKeccak256(
            ["bytes", "address"],
            [txHash.data, subjectOperatorCaller.address]
          );

          const isLogged = await feeExtension.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });
      });

      describe("when the caller is not the operator or methodologist", async () => {
        beforeEach(async () => {
          subjectOperatorCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject(subjectOperatorCaller)).to.be.revertedWith("Must be authorized address");
        });
      });
    });

    describe("#updateOperatorFeeRecipient", async () => {
      let subjectCaller: Account;
      let subjectOperatorFeeRecipient: Address;

      beforeEach(async () => {
        subjectCaller = operator;
        subjectOperatorFeeRecipient = (await getRandomAccount()).address;
      });

      async function subject(): Promise<ContractTransaction> {
        return await feeExtension
          .connect(subjectCaller.wallet)
          .updateOperatorFeeRecipient(subjectOperatorFeeRecipient);
      }

      it("sets the new operator fee recipient", async () => {
        await subject();

        const newOperatorFeeRecipient = await feeExtension.operatorFeeRecipient();
        expect(newOperatorFeeRecipient).to.eq(subjectOperatorFeeRecipient);
      });

      describe("when the new operator fee recipient is address zero", async () => {
        beforeEach(async () => {
          subjectOperatorFeeRecipient = ADDRESS_ZERO;
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Zero address not valid");
        });
      });

      describe("when the caller is not the operator", async () => {
        beforeEach(async () => {
          subjectCaller = methodologist;
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Must be operator");
        });
      });
    });
  });
});
Example #12
Source File: mutualUpgrade.spec.ts    From index-coop-smart-contracts with Apache License 2.0 4 votes vote down vote up
describe("MutualUpgrade", () => {
  let owner: Account;
  let methodologist: Account;
  let deployer: DeployHelper;

  let mutualUpgradeMock: MutualUpgradeMock;

  before(async () => {
    [
      owner,
      methodologist,
    ] = await getAccounts();

    deployer = new DeployHelper(owner.wallet);

    mutualUpgradeMock = await deployer.mocks.deployMutualUpgradeMock(owner.address, methodologist.address);
  });

  addSnapshotBeforeRestoreAfterEach();

  describe("#testMutualUpgrade", async () => {
    let subjectTestUint: BigNumber;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectTestUint = ONE;
      subjectCaller = owner;
    });

    async function subject(): Promise<ContractTransaction> {
      return mutualUpgradeMock.connect(subjectCaller.wallet).testMutualUpgrade(subjectTestUint);
    }

    describe("when the mutualUpgrade hash is not set", async () => {
      it("should register the initial mutual upgrade", async () => {
        const txHash = await subject();

        const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]);
        const isLogged = await mutualUpgradeMock.mutualUpgrades(expectedHash);

        expect(isLogged).to.be.true;
      });

      it("should not update the testUint", async () => {
        await subject();

        const currentInt = await mutualUpgradeMock.testUint();
        expect(currentInt).to.eq(ZERO);
      });

      it("emits a MutualUpgradeRegistered event", async () => {
        await expect(subject()).to.emit(mutualUpgradeMock, "MutualUpgradeRegistered");
      });
    });

    describe("when the mutualUpgrade hash is set", async () => {
      beforeEach(async () => {
        await subject();
        subjectCaller = methodologist;
      });

      it("should clear the mutualUpgrade hash", async () => {
        const txHash = await subject();

        const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]);
        const isLogged = await mutualUpgradeMock.mutualUpgrades(expectedHash);

        expect(isLogged).to.be.false;
      });

      it("should update the testUint", async () => {
        await subject();

        const currentTestUint = await mutualUpgradeMock.testUint();
        expect(currentTestUint).to.eq(subjectTestUint);
      });

      describe("when the same address calls it twice", async () => {
        beforeEach(async () => {
          subjectCaller = owner;
        });

        it("should stay logged", async () => {
          const txHash = await subject();

          const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]);
          const isLogged = await mutualUpgradeMock.mutualUpgrades(expectedHash);

          expect(isLogged).to.be.true;
        });

        it("should not change the integer value", async () => {
          await subject();

          const currentInt = await mutualUpgradeMock.testUint();
          expect(currentInt).to.eq(ZERO);
        });
      });
    });

    describe("when the sender is not one of the allowed addresses", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be authorized address");
      });
    });
  });
});
Example #13
Source File: baseManagerV2.spec.ts    From index-coop-smart-contracts with Apache License 2.0 4 votes vote down vote up
describe("BaseManagerV2", () => {
  let operator: Account;
  let methodologist: Account;
  let otherAccount: Account;
  let newManager: Account;
  let setV2Setup: SetFixture;

  let deployer: DeployHelper;
  let setToken: SetToken;

  let baseManager: BaseManagerV2;
  let baseExtension: BaseExtensionMock;

  async function validateMutualUprade(txHash: ContractTransaction, caller: Address) {
    const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, caller]);
    const isLogged = await baseManager.mutualUpgrades(expectedHash);
    expect(isLogged).to.be.true;
  }

  before(async () => {
    [
      operator,
      otherAccount,
      newManager,
      methodologist,
    ] = await getAccounts();

    deployer = new DeployHelper(operator.wallet);

    setV2Setup = getSetFixture(operator.address);
    await setV2Setup.initialize();

    setToken = await setV2Setup.createSetToken(
      [setV2Setup.dai.address],
      [ether(1)],
      [
        setV2Setup.issuanceModule.address,
        setV2Setup.streamingFeeModule.address,
        setV2Setup.governanceModule.address,
      ]
    );

    // Initialize modules
    await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO);
    await setV2Setup.governanceModule.initialize(setToken.address);

    const feeRecipient = operator.address;
    const maxStreamingFeePercentage = ether(.1);
    const streamingFeePercentage = ether(.02);
    const streamingFeeSettings = {
      feeRecipient,
      maxStreamingFeePercentage,
      streamingFeePercentage,
      lastStreamingFeeTimestamp: ZERO,
    };
    await setV2Setup.streamingFeeModule.initialize(setToken.address, streamingFeeSettings);

    // Deploy BaseManager
    baseManager = await deployer.manager.deployBaseManagerV2(
      setToken.address,
      operator.address,
      methodologist.address
    );

    // Transfer operatorship to BaseManager
    await setToken.setManager(baseManager.address);

    baseExtension = await deployer.mocks.deployBaseExtensionMock(baseManager.address);
  });

  addSnapshotBeforeRestoreAfterEach();

  describe("#constructor", async () => {
    let subjectSetToken: Address;
    let subjectOperator: Address;
    let subjectMethodologist: Address;

    beforeEach(async () => {
      subjectSetToken = setToken.address;
      subjectOperator = operator.address;
      subjectMethodologist = methodologist.address;
    });

    async function subject(): Promise<BaseManagerV2> {
      return await deployer.manager.deployBaseManagerV2(
        subjectSetToken,
        subjectOperator,
        subjectMethodologist
      );
    }

    it("should set the correct SetToken address", async () => {
      const retrievedICManager = await subject();

      const actualToken = await retrievedICManager.setToken();
      expect (actualToken).to.eq(subjectSetToken);
    });

    it("should set the correct Operator address", async () => {
      const retrievedICManager = await subject();

      const actualOperator = await retrievedICManager.operator();
      expect (actualOperator).to.eq(subjectOperator);
    });

    it("should set the correct Methodologist address", async () => {
      const retrievedICManager = await subject();

      const actualMethodologist = await retrievedICManager.methodologist();
      expect (actualMethodologist).to.eq(subjectMethodologist);
    });

    it("should not be initialized by default", async () => {
      const retrievedICManager = await subject();

      const initialized = await retrievedICManager.initialized();
      expect(initialized).to.be.false;
    });
  });

  describe("#authorizeInitialization", () => {
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectCaller = methodologist;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).authorizeInitialization();
    }

    it("sets initialized to true", async() => {
      const defaultInitialized = await baseManager.initialized();

      await subject();

      const updatedInitialized = await baseManager.initialized();

      expect(defaultInitialized).to.be.false;
      expect(updatedInitialized).to.be.true;
    });

    describe("when the caller is not the methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be methodologist");
      });
    });

    describe("when the manager is already initialized", async () => {
      beforeEach(async () => {
        await subject();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Initialization authorized");
      });
    });
  });

  describe("#setManager", async () => {
    let subjectNewManager: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewManager = newManager.address;
      subjectCaller = operator;
    });

    async function subject(caller: Account): Promise<any> {
      return baseManager.connect(caller.wallet).setManager(subjectNewManager);
    }

    it("should change the manager address", async () => {
      await subject(operator);
      await subject(methodologist);
      const manager = await setToken.manager();

      expect(manager).to.eq(newManager.address);
    });

    describe("when a single mutual upgrade party calls", () => {
      it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
        const txHash = await subject(operator);
        await validateMutualUprade(txHash, operator.address);
      });
    });

    describe("when passed manager is the zero address", async () => {
      beforeEach(async () => {
        subjectNewManager = ADDRESS_ZERO;
      });

      it("should revert", async () => {
        await subject(operator);
        await expect(subject(methodologist)).to.be.revertedWith("Zero address not valid");
      });
    });

    describe("when the caller is not the operator or the methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject(subjectCaller)).to.be.revertedWith("Must be authorized address");
      });
    });
  });

  describe("#addExtension", async () => {
    let subjectModule: Address;
    let subjectExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectExtension = baseExtension.address;
      subjectCaller = operator;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).addExtension(subjectExtension);
    }

    it("should add the extension address", async () => {
      await subject();
      const extensions = await baseManager.getExtensions();

      expect(extensions[0]).to.eq(baseExtension.address);
    });

    it("should set the extension mapping", async () => {
      await subject();
      const isExtension = await baseManager.isExtension(subjectExtension);

      expect(isExtension).to.be.true;
    });

    it("should emit the correct ExtensionAdded event", async () => {
      await expect(subject()).to.emit(baseManager, "ExtensionAdded").withArgs(baseExtension.address);
    });

    describe("when the extension already exists", async () => {
      beforeEach(async () => {
        await subject();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Extension already exists");
      });
    });

    describe("when extension has different manager address", async () => {
      beforeEach(async () => {
        subjectExtension = (await deployer.mocks.deployBaseExtensionMock(await getRandomAddress())).address;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Extension manager invalid");
      });
    });

    describe("when an emergency is in progress", async () => {
      beforeEach(async () => {
        baseManager.connect(operator.wallet);
        await baseManager.protectModule(subjectModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectModule);
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Upgrades paused by emergency");
      });
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = methodologist;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#removeExtension", async () => {
    let subjectModule: Address;
    let subjectAdditionalModule: Address;
    let subjectExtension: Address;
    let subjectAdditionalExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      await baseManager.connect(operator.wallet).addExtension(baseExtension.address);

      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectAdditionalModule = setV2Setup.issuanceModule.address;
      subjectExtension = baseExtension.address;
      subjectAdditionalExtension = (await deployer.mocks.deployBaseExtensionMock(baseManager.address)).address;
      subjectCaller = operator;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).removeExtension(subjectExtension);
    }

    it("should remove the extension address", async () => {
      await subject();
      const extensions = await baseManager.getExtensions();

      expect(extensions.length).to.eq(0);
    });

    it("should set the extension mapping", async () => {
      await subject();
      const isExtension = await baseManager.isExtension(subjectExtension);

      expect(isExtension).to.be.false;
    });

    it("should emit the correct ExtensionRemoved event", async () => {
      await expect(subject()).to.emit(baseManager, "ExtensionRemoved").withArgs(baseExtension.address);
    });

    describe("when the extension does not exist", async () => {
      beforeEach(async () => {
        subjectExtension = await getRandomAddress();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Extension does not exist");
      });
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = methodologist;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });

    describe("when the extension is authorized for a protected module", () => {
      beforeEach(() => {
        baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should revert", async() => {
        await expect(subject()).to.be.revertedWith("Extension used by protected module");
      });
    });

    // This test for the coverage report - hits an alternate branch condition the authorized
    // extensions search method....
    describe("when multiple extensionsa are authorized for multiple protected modules", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).protectModule(subjectAdditionalModule, [subjectAdditionalExtension]);
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should revert", async() => {
        await expect(subject()).to.be.revertedWith("Extension used by protected module");
      });
    });
  });

  describe("#authorizeExtension", () => {
    let subjectModule: Address;
    let subjectExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectCaller = operator;
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectExtension = baseExtension.address;
    });

    async function subject(caller: Account): Promise<any> {
      return baseManager.connect(caller.wallet).authorizeExtension(subjectModule, subjectExtension);
    }

    describe("when extension is not authorized and already added", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
        await baseManager.connect(operator.wallet).protectModule(subjectModule, []);
      });

      it("should authorize the extension", async () => {
        const initialAuthorization = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject(operator);
        await subject(methodologist);

        const finalAuthorization = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(initialAuthorization).to.be.false;
        expect(finalAuthorization).to.be.true;
      });

      it("should emit the correct ExtensionAuthorized event", async () => {
        await subject(operator);

        await expect(subject(methodologist)).to
          .emit(baseManager, "ExtensionAuthorized")
          .withArgs(subjectModule, subjectExtension);
      });
    });

    describe("when extension is not already added to the manager", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).protectModule(subjectModule, []);
      });

      it("should add and authorize the extension", async () => {
        const initialIsExtension = await baseManager.isExtension(subjectExtension);
        const initialAuthorization = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject(operator);
        await subject(methodologist);

        const finalIsExtension = await baseManager.isExtension(subjectExtension);
        const finalAuthorization = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(initialIsExtension).to.be.false;
        expect(initialAuthorization).to.be.false;
        expect(finalIsExtension).to.be.true;
        expect(finalAuthorization).to.be.true;
      });
    });

    describe("when the extension is already authorized for target module", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should revert", async () => {
        await subject(operator);
        await expect(subject(methodologist)).to.be.revertedWith("Extension already authorized");
      });
    });

    describe("when target module is not protected", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
      });

      it("should revert", async () => {
        const isProtected = await baseManager.protectedModules(subjectModule);

        await subject(operator);

        await expect(isProtected).to.be.false;
        await expect(subject(methodologist)).to.be.revertedWith("Module not protected");
      });
    });

    describe("when a single mutual upgrade party calls", () => {
      it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
        const txHash = await subject(operator);
        await validateMutualUprade(txHash, operator.address);
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject(subjectCaller)).to.be.revertedWith("Must be authorized");
      });
    });
  });

  describe("#revokeExtensionAuthorization", () => {
    let subjectModule: Address;
    let subjectAdditionalModule: Address;
    let subjectExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectCaller = operator;
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectAdditionalModule = setV2Setup.issuanceModule.address;
      subjectExtension = baseExtension.address;
    });

    async function subject(caller: Account): Promise<any> {
      return baseManager.connect(caller.wallet).revokeExtensionAuthorization(subjectModule, subjectExtension);
    }

    describe("when extension is authorized", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should revoke extension authorization", async () => {
        const initialAuthorization = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject(operator);
        await subject(methodologist);

        const finalAuthorization = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(initialAuthorization).to.be.true;
        expect(finalAuthorization).to.be.false;
      });

      it("should emit the correct ExtensionAuthorizationRevoked event", async () => {
        await subject(operator);

        await expect(subject(methodologist)).to
          .emit(baseManager, "ExtensionAuthorizationRevoked")
          .withArgs(subjectModule, subjectExtension);
      });
    });

    describe("when an extension is shared by protected modules", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
        await baseManager.connect(operator.wallet).protectModule(subjectAdditionalModule, [subjectExtension]);
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should only revoke authorization for the specified module", async () => {
        const initialAuth = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);
        const initialAdditionalAuth = await baseManager.isAuthorizedExtension(subjectAdditionalModule, subjectExtension);

        await subject(operator);
        await subject(methodologist);

        const finalAuth = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);
        const finalAdditionalAuth = await baseManager.isAuthorizedExtension(subjectAdditionalModule, subjectExtension);

        expect(initialAuth).to.be.true;
        expect(initialAdditionalAuth).to.be.true;
        expect(finalAuth).to.be.false;
        expect(finalAdditionalAuth).to.be.true;
      });
    });

    describe("when extension is not added to the manager", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).protectModule(subjectModule, []);
      });

      it("should revert", async () => {
        const initialExtensionStatus = await baseManager.connect(operator.wallet).isExtension(subjectExtension);

        await subject(operator);

        await expect(initialExtensionStatus).to.be.false;
        await expect(subject(methodologist)).to.be.revertedWith("Extension does not exist");
      });
    });

    describe("when the extension is not authorized for target module", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
        await baseManager.connect(operator.wallet).protectModule(subjectModule, []);
      });

      it("should revert", async () => {
        const initialAuthorization = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject(operator);
        await expect(initialAuthorization).to.be.false;
        await expect(subject(methodologist)).to.be.revertedWith("Extension not authorized");
      });
    });

    describe("when target module is not protected", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
      });

      it("should revert", async () => {
        const isProtected = await baseManager.protectedModules(subjectModule);

        await subject(operator);

        await expect(isProtected).to.be.false;
        await expect(subject(methodologist)).to.be.revertedWith("Module not protected");
      });
    });

    describe("when a single mutual upgrade party calls", () => {
      it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
        const txHash = await subject(operator);
        await validateMutualUprade(txHash, operator.address);
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject(subjectCaller)).to.be.revertedWith("Must be authorized");
      });
    });
  });

  describe("#addModule", async () => {
    let subjectModule: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      await setV2Setup.controller.addModule(otherAccount.address);

      subjectModule = otherAccount.address;
      subjectCaller = operator;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).addModule(subjectModule);
    }

    it("should add the module to the SetToken", async () => {
      await subject();
      const isModule = await setToken.isPendingModule(subjectModule);
      expect(isModule).to.eq(true);
    });

    describe("when an emergency is in progress", async () => {
      beforeEach(async () => {
        subjectModule = setV2Setup.streamingFeeModule.address;
        await baseManager.protectModule(subjectModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectModule);
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Upgrades paused by emergency");
      });
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = methodologist;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#emergencyRemoveProtectedModule", () => {
    let subjectModule: Address;
    let subjectAdditionalModule: Address;
    let subjectExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectCaller = operator;
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectAdditionalModule = setV2Setup.governanceModule.address; // Removable
      subjectExtension = baseExtension.address;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).emergencyRemoveProtectedModule(subjectModule);
    }

    describe("when module is protected", async () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should remove the module from the set token", async () => {
        await subject();
        const isModule = await setToken.isInitializedModule(subjectModule);
        expect(isModule).to.eq(false);
      });

      it("should unprotect the module", async () => {
        await subject();
        const isProtected = await baseManager.protectedModules(subjectModule);
        expect(isProtected).to.be.false;
      });

      it("should clear the protected modules authorized extension registries", async () => {
        const initialAuthorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectModule);
        const initialIsAuthorized = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject();

        const finalAuthorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectModule);
        const finalIsAuthorized = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(initialAuthorizedExtensionsList.length).equals(1);
        expect(initialIsAuthorized).to.be.true;
        expect(finalAuthorizedExtensionsList.length).equals(0);
        expect(finalIsAuthorized).to.be.false;
      });

      it("should not preserve any settings if same module is removed and restored", async () => {
        await subject();

        await baseManager.connect(methodologist.wallet).resolveEmergency();
        await baseManager.connect(operator.wallet).addModule(subjectModule);

        // Invoke initialize on streamingFeeModule
        const feeRecipient = operator.address;
        const maxStreamingFeePercentage = ether(.1);
        const streamingFeePercentage = ether(.02);
        const streamingFeeSettings = {
          feeRecipient,
          maxStreamingFeePercentage,
          streamingFeePercentage,
          lastStreamingFeeTimestamp: ZERO,
        };

        const initializeData = setV2Setup
          .streamingFeeModule
          .interface
          .encodeFunctionData("initialize", [setToken.address, streamingFeeSettings]);

        await baseManager.connect(methodologist.wallet).authorizeInitialization();
        await baseExtension.interactManager(subjectModule, initializeData);
        await baseManager.connect(operator.wallet).protectModule(subjectModule, []);

        const authorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectModule);
        const isAuthorized = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(authorizedExtensionsList.length).equals(0);
        expect(isAuthorized).to.be.false;
      });

      it("should increment the emergencies counter", async () => {
        const initialEmergencies = await baseManager.emergencies();

        await subject();

        const finalEmergencies = await baseManager.emergencies();

        expect(initialEmergencies.toNumber()).equals(0);
        expect(finalEmergencies.toNumber()).equals(1);
      });

      it("should emit the correct EmergencyRemovedProtectedModule event", async () => {
        await expect(subject()).to
          .emit(baseManager, "EmergencyRemovedProtectedModule")
          .withArgs(subjectModule);
      });
    });

    describe("when an emergency is already in progress", async () => {
      beforeEach(async () => {
        baseManager.connect(operator.wallet);

        await baseManager.protectModule(subjectModule, []);
        await baseManager.protectModule(subjectAdditionalModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectAdditionalModule);
      });

      it("should increment the emergencies counter", async () => {
        const initialEmergencies = await baseManager.emergencies();

        await subject();

        const finalEmergencies = await baseManager.emergencies();

        expect(initialEmergencies.toNumber()).equals(1);
        expect(finalEmergencies.toNumber()).equals(2);
      });
    });

    describe("when module is not protected", () => {
      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Module not protected");
      });
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = methodologist;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#protectModule", () => {
    let subjectModule: Address;
    let subjectAdditionalModule: Address;
    let subjectExtension: Address;
    let subjectAuthorizedExtensions: Address[];
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectCaller = operator;
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectExtension = baseExtension.address;
      subjectAdditionalModule = setV2Setup.governanceModule.address; // Removable
      subjectAuthorizedExtensions = [];
    });

    async function subject(): Promise<any> {
      return baseManager
        .connect(subjectCaller.wallet)
        .protectModule(subjectModule, subjectAuthorizedExtensions);
    }

    describe("when module already added, no extensions", () => {
      it("should protect the module", async () => {
        const initialIsProtected = await baseManager.protectedModules(subjectModule);
        const initialProtectedModulesList = await baseManager.getProtectedModules();

        await subject();

        const finalIsProtected = await baseManager.protectedModules(subjectModule);
        const finalProtectedModulesList = await baseManager.getProtectedModules();

        expect(initialIsProtected).to.be.false;
        expect(finalIsProtected).to.be.true;
        expect(initialProtectedModulesList.length).equals(0);
        expect(finalProtectedModulesList.length).equals(1);
      });
    });

    describe("when module already added, with non-added extension", () => {
      beforeEach(() => {
        subjectAuthorizedExtensions = [subjectExtension];
      });
      it("should add and authorize the extension", async () => {
        const initialIsExtension = await baseManager.isExtension(subjectExtension);
        const initialIsAuthorizedExtension = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject();

        const finalIsExtension = await baseManager.isExtension(subjectExtension);
        const finalIsAuthorizedExtension = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(initialIsExtension).to.be.false;
        expect(finalIsExtension).to.be.true;
        expect(initialIsAuthorizedExtension).to.be.false;
        expect(finalIsAuthorizedExtension).to.be.true;
      });

      // With extensions...
      it("should emit the correct ModuleProtected event", async () => {
        await expect(subject()).to
          .emit(baseManager, "ModuleProtected")
          .withArgs(subjectModule, subjectAuthorizedExtensions);
      });
    });

    describe("when module and extension already added", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).addExtension(subjectExtension);
        subjectAuthorizedExtensions = [subjectExtension];
      });

      it("should authorize the extension", async () => {
        const initialIsExtension = await baseManager.isExtension(subjectExtension);
        const initialIsAuthorizedExtension = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject();

        const finalIsAuthorizedExtension = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(initialIsExtension).to.be.true;
        expect(initialIsAuthorizedExtension).to.be.false;
        expect(finalIsAuthorizedExtension).to.be.true;
      });
    });

    describe("when module not added", () => {
      beforeEach(async () => {
        await baseManager.removeModule(subjectModule);
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Module not added yet");
      });
    });

    describe("when module already protected", () => {
      beforeEach(async () => {
        await baseManager.protectModule(subjectModule, []);
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Module already protected");
      });
    });

    describe("when an emergency is in progress", async () => {
      beforeEach(async () => {
        await baseManager.protectModule(subjectAdditionalModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectAdditionalModule);
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Upgrades paused by emergency");
      });
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = methodologist;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#unProtectModule", () => {
    let subjectModule: Address;
    let subjectExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectCaller = methodologist;
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectExtension = baseExtension.address;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).unProtectModule(subjectModule);
    }

    describe("when module is protected", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should *not* remove the module from the set token", async () => {
        await subject();
        const isModule = await setToken.isInitializedModule(subjectModule);
        expect(isModule).to.be.true;
      });

      it("should unprotect the module", async () => {
        await subject();
        const isProtected = await baseManager.protectedModules(subjectModule);
        expect(isProtected).to.be.false;
      });

      it("should clear the protected modules authorized extension registries", async () => {
        const initialAuthorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectModule);
        const initialIsAuthorized = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        await subject();

        const finalAuthorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectModule);
        const finalIsAuthorized = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(initialAuthorizedExtensionsList.length).equals(1);
        expect(initialIsAuthorized).to.be.true;
        expect(finalAuthorizedExtensionsList.length).equals(0);
        expect(finalIsAuthorized).to.be.false;
      });

      it("should not preserve any settings if same module is removed and restored", async () => {
        await subject();

        // Restore without extension
        await baseManager.protectModule(subjectModule, []);

        const authorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectModule);
        const isAuthorized = await baseManager.isAuthorizedExtension(subjectModule, subjectExtension);

        expect(authorizedExtensionsList.length).equals(0);
        expect(isAuthorized).to.be.false;
      });

      it("should emit the correct ModuleUnprotected event", async () => {
        await expect(subject()).to
          .emit(baseManager, "ModuleUnprotected")
          .withArgs(subjectModule);
      });
    });

    describe("when module is not protected", () => {
      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Module not protected");
      });
    });

    describe("when the caller is not the methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = operator;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be methodologist");
      });
    });
  });

  describe("#replaceProtectedModule", () => {
    let subjectOldModule: Address;
    let subjectNewModule: Address;
    let subjectOldExtension: Address;
    let subjectNewExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      await setV2Setup.controller.addModule(otherAccount.address);

      subjectCaller = operator;
      subjectOldModule = setV2Setup.streamingFeeModule.address;
      subjectNewModule = otherAccount.address;
      subjectOldExtension = baseExtension.address;
      subjectNewExtension = (await deployer.mocks.deployBaseExtensionMock(baseManager.address)).address;
    });

    async function subject(caller: Account): Promise<any> {
      return baseManager
        .connect(caller.wallet)
        .replaceProtectedModule(subjectOldModule, subjectNewModule, [subjectNewExtension]);
    }

    describe("when old module is protected", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).protectModule(subjectOldModule, [subjectOldExtension]);
      });

      describe("when new module is not added", () => {
        it("should add new module to setToken", async () => {
          const initialModuleAdded = await setToken.isPendingModule(subjectNewModule);

          await subject(operator);
          await subject(methodologist);

          const finalModuleAdded = await setToken.isPendingModule(subjectNewModule);

          expect(initialModuleAdded).to.be.false;
          expect(finalModuleAdded).to.be.true;
        });

        it("should remove old module from setToken", async () => {
          const initialModuleAdded = await setToken.isInitializedModule(subjectOldModule);

          await subject(operator);
          await subject(methodologist);

          const finalModuleAdded = await setToken.isInitializedModule(subjectOldModule);

          expect(initialModuleAdded).to.be.true;
          expect(finalModuleAdded).to.be.false;
        });

        it("should protect the module", async () => {
          const initialIsProtected = await baseManager.protectedModules(subjectNewModule);
          const initialProtectedModulesList = await baseManager.getProtectedModules();

          await subject(operator);
          await subject(methodologist);

          const finalIsProtected = await baseManager.protectedModules(subjectNewModule);
          const finalProtectedModulesList = await baseManager.getProtectedModules();

          expect(initialIsProtected).to.be.false;
          expect(finalIsProtected).to.be.true;
          expect(initialProtectedModulesList[0]).equals(subjectOldModule);
          expect(finalProtectedModulesList[0]).equals(subjectNewModule);
        });

        it("should unprotect the old module", async () => {
          await subject(operator);
          await subject(methodologist);

          const isProtected = await baseManager.protectedModules(subjectOldModule);
          expect(isProtected).to.be.false;
        });

        it("should clear the old modules authorized extension registries", async () => {
          const initialAuthorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectOldModule);
          const initialIsAuthorized = await baseManager.isAuthorizedExtension(subjectOldModule, subjectOldExtension);

          await subject(operator);
          await subject(methodologist);

          const finalAuthorizedExtensionsList = await baseManager.getAuthorizedExtensions(subjectOldModule);
          const finalIsAuthorized = await baseManager.isAuthorizedExtension(subjectOldModule, subjectOldExtension);

          expect(initialAuthorizedExtensionsList.length).equals(1);
          expect(initialIsAuthorized).to.be.true;
          expect(finalAuthorizedExtensionsList.length).equals(0);
          expect(finalIsAuthorized).to.be.false;
        });

        it("should add and authorize the new module extension", async () => {
          const initialIsExtension = await baseManager.isExtension(subjectNewExtension);
          const initialIsAuthorizedExtension = await baseManager.isAuthorizedExtension(
            subjectNewModule, subjectNewExtension
          );

          await subject(operator);
          await subject(methodologist);

          const finalIsExtension = await baseManager.isExtension(subjectNewExtension);
          const finalIsAuthorizedExtension = await baseManager.isAuthorizedExtension(
            subjectNewModule,
            subjectNewExtension
          );

          expect(initialIsExtension).to.be.false;
          expect(finalIsExtension).to.be.true;
          expect(initialIsAuthorizedExtension).to.be.false;
          expect(finalIsAuthorizedExtension).to.be.true;
        });

        it("should emit the correct ReplacedProtectedModule event", async () => {
          await subject(operator);

          await expect(subject(methodologist)).to
            .emit(baseManager, "ReplacedProtectedModule")
            .withArgs(subjectOldModule, subjectNewModule, [subjectNewExtension]);
        });
      });

      describe("when the new module is already added", async () => {
        beforeEach(async () => {
          await baseManager.addModule(subjectNewModule);
        });

        it("should revert", async () => {
          await subject(operator);
          await expect(subject(methodologist)).to.be.revertedWith("Module must not be added");
        });
      });
    });

    describe("when old module is not protected", () => {
      it("should revert", async () => {
        await subject(operator);
        await expect(subject(methodologist)).to.be.revertedWith("Module not protected");
      });
    });

    describe("when a single mutual upgrade party calls", () => {
      it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
        const txHash = await subject(operator);
        await validateMutualUprade(txHash, operator.address);
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject(subjectCaller)).to.be.revertedWith("Must be authorized");
      });
    });
  });

  describe("#emergencyReplaceProtectedModule", () => {
    let subjectOldModule: Address;
    let subjectAdditionalOldModule: Address;
    let subjectNewModule: Address;
    let subjectNewExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      await setV2Setup.controller.addModule(otherAccount.address);

      subjectCaller = operator;
      subjectOldModule = setV2Setup.streamingFeeModule.address;
      subjectAdditionalOldModule = setV2Setup.governanceModule.address; // Removable
      subjectNewModule = otherAccount.address;
      subjectNewExtension = (await deployer.mocks.deployBaseExtensionMock(baseManager.address)).address;
    });

    async function subject(caller: Account): Promise<any> {
      return baseManager
        .connect(caller.wallet)
        .emergencyReplaceProtectedModule(subjectNewModule, [subjectNewExtension]);
    }

    describe("when new module is not added", () => {
      beforeEach(async () => {
        // Trigger emergency
        baseManager.connect(operator.wallet);
        await baseManager.protectModule(subjectOldModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectOldModule);
      });

      it("should add module to setToken", async () => {
        const initialModuleAdded = await setToken.isPendingModule(subjectNewModule);

        await subject(operator);
        await subject(methodologist);

        const finalModuleAdded = await setToken.isPendingModule(subjectNewModule);

        expect(initialModuleAdded).to.be.false;
        expect(finalModuleAdded).to.be.true;
      });

      it("should protect the module", async () => {
        const initialIsProtected = await baseManager.protectedModules(subjectNewModule);

        await subject(operator);
        await subject(methodologist);

        const finalIsProtected = await baseManager.protectedModules(subjectNewModule);

        expect(initialIsProtected).to.be.false;
        expect(finalIsProtected).to.be.true;
      });

      it("should add and authorize the new module extension", async () => {
        const initialIsExtension = await baseManager.isExtension(subjectNewExtension);
        const initialIsAuthorizedExtension = await baseManager.isAuthorizedExtension(
          subjectNewModule, subjectNewExtension
        );

        await subject(operator);
        await subject(methodologist);

        const finalIsExtension = await baseManager.isExtension(subjectNewExtension);
        const finalIsAuthorizedExtension = await baseManager.isAuthorizedExtension(
          subjectNewModule,
          subjectNewExtension
        );

        expect(initialIsExtension).to.be.false;
        expect(finalIsExtension).to.be.true;
        expect(initialIsAuthorizedExtension).to.be.false;
        expect(finalIsAuthorizedExtension).to.be.true;
      });

      it("should decrement the emergencies counter", async() => {
        const initialEmergencies = await baseManager.emergencies();

        await subject(operator);
        await subject(methodologist);

        const finalEmergencies = await baseManager.emergencies();

        expect(initialEmergencies.toNumber()).equals(1);
        expect(finalEmergencies.toNumber()).equals(0);
      });

      it("should emit the correct EmergencyReplacedProtectedModule event", async () => {
        await subject(operator);

        await expect(subject(methodologist)).to
          .emit(baseManager, "EmergencyReplacedProtectedModule")
          .withArgs(subjectNewModule, [subjectNewExtension]);
      });
    });

    describe("when the new module is already added", async () => {
      beforeEach(async () => {
        baseManager.connect(operator.wallet);
        await baseManager.addModule(subjectNewModule);
        await baseManager.protectModule(subjectOldModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectOldModule);
      });

      it("should revert", async () => {
        await subject(operator);
        await expect(subject(methodologist)).to.be.revertedWith("Module must not be added");
      });
    });

    describe("when an emergency is not in progress", async () => {
      it("should revert", async () => {
        await subject(operator);
        await expect(subject(methodologist)).to.be.revertedWith("Not in emergency");
      });
    });

    describe("when more than one emergency is in progress", async () => {
      beforeEach(async () => {
        baseManager.connect(operator.wallet);
        await baseManager.protectModule(subjectOldModule, []);
        await baseManager.protectModule(subjectAdditionalOldModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectOldModule);
        await baseManager.emergencyRemoveProtectedModule(subjectAdditionalOldModule);
      });

      it("should remain in an emergency state after replacement", async () => {
        const initialEmergencies = await baseManager.emergencies();

        await subject(operator);
        await subject(methodologist);

        const finalEmergencies = await baseManager.emergencies();

        expect(initialEmergencies.toNumber()).equals(2);
        expect(finalEmergencies.toNumber()).equals(1);
      });
    });

    describe("when a single mutual upgrade party calls", () => {
      it("should log the proposed streaming fee hash in the mutualUpgrades mapping", async () => {
        const txHash = await subject(operator);
        await validateMutualUprade(txHash, operator.address);
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject(subjectCaller)).to.be.revertedWith("Must be authorized");
      });
    });
  });

  describe("#resolveEmergency", () => {
    let subjectModule: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectCaller = methodologist;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).resolveEmergency();
    }

    describe("when an emergency is in progress", () => {
      beforeEach(async () => {
        await baseManager.protectModule(subjectModule, []);
        await baseManager.emergencyRemoveProtectedModule(subjectModule);
      });

      it("should decrement the emergency counter", async () => {
        const initialEmergencies = await baseManager.emergencies();

        await subject();

        const finalEmergencies = await baseManager.emergencies();

        expect(initialEmergencies.toNumber()).equals(1);
        expect(finalEmergencies.toNumber()).equals(0);
      });

      it("should emit the correct EmergencyResolved event", async () => {
        await expect(subject()).to.emit(baseManager, "EmergencyResolved");
      });
    });

    describe("when an emergency is *not* in progress", async () => {
      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Not in emergency");
      });
    });

    describe("when the caller is not the methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = operator;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be methodologist");
      });
    });
  });

  describe("#interactManager", async () => {
    let subjectModule: Address;
    let subjectExtension: Address;
    let subjectCallData: Bytes;

    beforeEach(async () => {
      await baseManager.connect(operator.wallet).addExtension(baseExtension.address);

      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectExtension = baseExtension.address;

      // Invoke update fee recipient
      subjectCallData = setV2Setup.streamingFeeModule.interface.encodeFunctionData("updateFeeRecipient", [
        setToken.address,
        otherAccount.address,
      ]);
    });

    async function subject(): Promise<any> {
      return baseExtension.connect(operator.wallet).interactManager(subjectModule, subjectCallData);
    }

    context("when the manager is initialized", () => {
      beforeEach(async() => {
        await baseManager.connect(methodologist.wallet).authorizeInitialization();
      });

      it("should call updateFeeRecipient on the streaming fee module from the SetToken", async () => {
        await subject();
        const feeStates = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        expect(feeStates.feeRecipient).to.eq(otherAccount.address);
      });

      describe("when the caller is not an extension", async () => {
        beforeEach(async () => {
          await baseManager.connect(operator.wallet).removeExtension(baseExtension.address);
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Must be extension");
        });
      });

      describe("when the extension tries to call the SetToken", async () => {
        beforeEach(async () => {
          subjectModule = setToken.address;
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Extensions cannot call SetToken");
        });
      });
    });

    context("when the manager is not initialized", () => {
      it("updateFeeRecipient should revert", async () => {
        await expect(subject()).to.be.revertedWith("Manager not initialized");
      });
    });

    context("when the module is protected and extension is authorized", () => {
      beforeEach(async () => {
        await baseManager.connect(methodologist.wallet).authorizeInitialization();
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("updateFeeRecipient should succeed", async () => {
        await subject();
        const feeStates = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        expect(feeStates.feeRecipient).to.eq(otherAccount.address);
      });
    });

    context("when the module is protected and extension is not authorized", () => {
      beforeEach(async () => {
        await baseManager.connect(methodologist.wallet).authorizeInitialization();
        await baseManager.connect(operator.wallet).protectModule(subjectModule, []);
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Extension not authorized for module");
      });
    });
  });

  describe("#transferTokens", async () => {
    let subjectCaller: Account;
    let subjectToken: Address;
    let subjectDestination: Address;
    let subjectAmount: BigNumber;

    beforeEach(async () => {
      await baseManager.connect(operator.wallet).addExtension(baseExtension.address);

      subjectCaller = operator;
      subjectToken = setV2Setup.weth.address;
      subjectDestination = otherAccount.address;
      subjectAmount = ether(1);

      await setV2Setup.weth.transfer(baseManager.address, subjectAmount);
    });

    async function subject(): Promise<ContractTransaction> {
      return baseExtension.connect(subjectCaller.wallet).testInvokeManagerTransfer(
        subjectToken,
        subjectDestination,
        subjectAmount
      );
    }

    it("should send the given amount from the manager to the address", async () => {
      const preManagerAmount = await setV2Setup.weth.balanceOf(baseManager.address);
      const preDestinationAmount = await setV2Setup.weth.balanceOf(subjectDestination);

      await subject();

      const postManagerAmount = await setV2Setup.weth.balanceOf(baseManager.address);
      const postDestinationAmount = await setV2Setup.weth.balanceOf(subjectDestination);

      expect(preManagerAmount.sub(postManagerAmount)).to.eq(subjectAmount);
      expect(postDestinationAmount.sub(preDestinationAmount)).to.eq(subjectAmount);
    });

    describe("when the caller is not an extension", async () => {
        beforeEach(async () => {
          await baseManager.connect(operator.wallet).removeExtension(baseExtension.address);
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Must be extension");
        });
      });
  });

  describe("#removeModule", async () => {
    let subjectModule: Address;
    let subjectExtension: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectExtension = baseExtension.address;
      subjectCaller = operator;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).removeModule(subjectModule);
    }

    it("should remove the module from the SetToken", async () => {
      await subject();
      const isModule = await setToken.isInitializedModule(subjectModule);
      expect(isModule).to.eq(false);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });

    describe("when the module is protected module", () => {
      beforeEach(async () => {
        await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
      });

      it("should revert", async() => {
        await expect(subject()).to.be.revertedWith("Module protected");
      });
    });
  });

  describe("#setMethodologist", async () => {
    let subjectNewMethodologist: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewMethodologist = await getRandomAddress();
      subjectCaller = methodologist;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).setMethodologist(subjectNewMethodologist);
    }

    it("should set the new methodologist", async () => {
      await subject();
      const actualIndexModule = await baseManager.methodologist();
      expect(actualIndexModule).to.eq(subjectNewMethodologist);
    });

    it("should emit the correct MethodologistChanged event", async () => {
      await expect(subject()).to.emit(baseManager, "MethodologistChanged").withArgs(methodologist.address, subjectNewMethodologist);
    });

    describe("when the caller is not the methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be methodologist");
      });
    });
  });

  describe("#setOperator", async () => {
    let subjectNewOperator: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewOperator = await getRandomAddress();
      subjectCaller = operator;
    });

    async function subject(): Promise<any> {
      return baseManager.connect(subjectCaller.wallet).setOperator(subjectNewOperator);
    }

    it("should set the new operator", async () => {
      await subject();
      const actualIndexModule = await baseManager.operator();
      expect(actualIndexModule).to.eq(subjectNewOperator);
    });

    it("should emit the correct OperatorChanged event", async () => {
      await expect(subject()).to.emit(baseManager, "OperatorChanged").withArgs(operator.address, subjectNewOperator);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("E2E: deployment, configuration, methodologist authorization, use", async () => {
    let subjectSetToken: Address;
    let subjectExtension: StreamingFeeSplitExtension;
    let subjectModule: Address;
    let subjectOperator: Address;
    let subjectMethodologist: Address;
    let subjectFeeSplit: BigNumber;
    let subjectOperatorFeeRecipient: Address;
    let subjectManager: BaseManagerV2;

    beforeEach(async () => {
      subjectSetToken = setToken.address;
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectOperator = operator.address;
      subjectMethodologist = methodologist.address;
      subjectFeeSplit = ether(.7);
      subjectOperatorFeeRecipient = operator.address;

      // Deploy new manager
      subjectManager = await deployer.manager.deployBaseManagerV2(
        subjectSetToken,
        subjectOperator,
        subjectMethodologist
      );

      // Deploy new fee extension
      subjectExtension = await deployer.extensions.deployStreamingFeeSplitExtension(
        subjectManager.address,
        subjectModule,
        subjectFeeSplit,
        subjectOperatorFeeRecipient
      );

      // Operator protects module and adds extension
      await subjectManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension.address]);

      // Methodologist authorizes new manager
      await subjectManager.connect(methodologist.wallet).authorizeInitialization();

      // Transfer ownership from old to new manager
      await baseManager.connect(operator.wallet).setManager(subjectManager.address);
      await baseManager.connect(methodologist.wallet).setManager(subjectManager.address);
    });

    // Makes mutual upgrade call which routes call to module via interactManager
    async function subject(): Promise<void> {
      await subjectExtension.connect(operator.wallet).updateFeeRecipient(subjectExtension.address);
      await subjectExtension.connect(methodologist.wallet).updateFeeRecipient(subjectExtension.address);
    }

    it("allows protected calls", async() => {
      const initialFeeRecipient = (await setV2Setup.streamingFeeModule.feeStates(subjectSetToken)).feeRecipient;

      await subject();

      const finalFeeRecipient = (await setV2Setup.streamingFeeModule.feeStates(subjectSetToken)).feeRecipient;

      expect(initialFeeRecipient).to.equal(operator.address);
      expect(finalFeeRecipient).to.equal(subjectExtension.address);
    });
  });
});
Example #14
Source File: icManager.spec.ts    From index-coop-smart-contracts with Apache License 2.0 4 votes vote down vote up
describe("ICManager", () => {
  let owner: Account;
  let methodologist: Account;
  let otherAccount: Account;
  let newManager: Account;
  let trader: Account;
  let setV2Setup: SetFixture;

  let deployer: DeployHelper;
  let setToken: SetToken;
  let setTokensIssued: BigNumber;
  let indexModule: SingleIndexModule;

  let icManager: ICManager;
  let operatorFeeSplit: BigNumber;

  before(async () => {
    [
      owner,
      otherAccount,
      newManager,
      methodologist,
      trader,
    ] = await getAccounts();

    deployer = new DeployHelper(owner.wallet);

    setV2Setup = getSetFixture(owner.address);
    await setV2Setup.initialize();

    indexModule = await deployer.setV2.deploySingleIndexModule(
      setV2Setup.controller.address,
      setV2Setup.weth.address,
      (await getRandomAccount()).address, // TODO
      (await getRandomAccount()).address,
      (await getRandomAccount()).address,
    );
    await setV2Setup.controller.addModule(indexModule.address);

    setToken = await setV2Setup.createSetToken(
      [setV2Setup.dai.address],
      [ether(1)],
      [setV2Setup.issuanceModule.address, setV2Setup.streamingFeeModule.address, indexModule.address]
    );

    // Initialize modules
    await setV2Setup.issuanceModule.initialize(setToken.address, ADDRESS_ZERO);
    const feeRecipient = owner.address;
    const maxStreamingFeePercentage = ether(.1);
    const streamingFeePercentage = ether(.02);
    const streamingFeeSettings = {
      feeRecipient,
      maxStreamingFeePercentage,
      streamingFeePercentage,
      lastStreamingFeeTimestamp: ZERO,
    };
    await setV2Setup.streamingFeeModule.initialize(setToken.address, streamingFeeSettings);

    await indexModule.initialize(setToken.address);

    // Issue some Sets
    setTokensIssued = ether(10);
    operatorFeeSplit = ether(0.7); // 70% fee split
    await setV2Setup.issuanceModule.issue(setToken.address, setTokensIssued, otherAccount.address);

    // Deploy ICManager
    icManager = await deployer.manager.deployICManager(
      setToken.address,
      indexModule.address,
      setV2Setup.streamingFeeModule.address,
      owner.address,
      methodologist.address,
      operatorFeeSplit
    );

    // Update streaming fee recipient to IcManager
    await setV2Setup.streamingFeeModule.updateFeeRecipient(setToken.address, icManager.address);
    // Transfer ownership to IcManager
    await setToken.setManager(icManager.address);
  });

  addSnapshotBeforeRestoreAfterEach();

  describe("#constructor", async () => {
    let subjectSetToken: Address;
    let subjectIndexModule: Address;
    let subjectStreamingFeeModule: Address;
    let subjectOperator: Address;
    let subjectMethodologist: Address;
    let subjectOperatorFeeSplit: BigNumber;

    beforeEach(async () => {
      subjectSetToken = setToken.address;
      subjectIndexModule = indexModule.address;
      subjectStreamingFeeModule = setV2Setup.streamingFeeModule.address;
      subjectOperator = owner.address;
      subjectMethodologist = methodologist.address;
      subjectOperatorFeeSplit = ether(0.7);
    });

    async function subject(): Promise<ICManager> {
      return await deployer.manager.deployICManager(
        subjectSetToken,
        subjectIndexModule,
        subjectStreamingFeeModule,
        subjectOperator,
        subjectMethodologist,
        subjectOperatorFeeSplit
      );
    }

    it("should set the correct SetToken address", async () => {
      const retrievedICManager = await subject();

      const actualToken = await retrievedICManager.setToken();
      expect (actualToken).to.eq(subjectSetToken);
    });

    it("should set the correct IndexModule address", async () => {
      const retrievedICManager = await subject();

      const actualIndexModule = await retrievedICManager.indexModule();
      expect (actualIndexModule).to.eq(subjectIndexModule);
    });

    it("should set the correct StreamingFeeModule address", async () => {
      const retrievedICManager = await subject();

      const actualStreamingFeeModule = await retrievedICManager.feeModule();
      expect (actualStreamingFeeModule).to.eq(subjectStreamingFeeModule);
    });

    it("should set the correct Operator address", async () => {
      const retrievedICManager = await subject();

      const actualOperator = await retrievedICManager.operator();
      expect (actualOperator).to.eq(subjectOperator);
    });

    it("should set the correct Methodologist address", async () => {
      const retrievedICManager = await subject();

      const actualMethodologist = await retrievedICManager.methodologist();
      expect (actualMethodologist).to.eq(subjectMethodologist);
    });

    it("should set the correct Coop Fee Split address", async () => {
      const retrievedICManager = await subject();

      const actualOperatorFeeSplit = await retrievedICManager.operatorFeeSplit();
      expect (actualOperatorFeeSplit).to.eq(subjectOperatorFeeSplit);
    });

    describe("when operator fee split is greater than 1e18", async () => {
      beforeEach(async () => {
        subjectOperatorFeeSplit = ether(1.1);
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Operator Fee Split must be less than 1e18");
      });
    });
  });

  describe("#startRebalance", async () => {
    let subjectNewComponents: Address[];
    let subjectNewComponentsTargetUnits: BigNumber[];
    let subjectOldComponentsTargetUnits: BigNumber[];
    let subjectPositionMultiplier: BigNumber;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewComponents = [setV2Setup.usdc.address];
      subjectNewComponentsTargetUnits = [BigNumber.from(500000)];
      subjectOldComponentsTargetUnits = [ether(.5)];
      subjectPositionMultiplier = ether(1);
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.startRebalance(
        subjectNewComponents,
        subjectNewComponentsTargetUnits,
        subjectOldComponentsTargetUnits,
        subjectPositionMultiplier
      );
    }

    it("should set the asset info in the index module", async () => {
      await subject();
      const assetInfoUSDC = await indexModule.assetInfo(subjectNewComponents[0]);
      const assetInfoDai = await indexModule.assetInfo(setV2Setup.dai.address);
      expect(assetInfoUSDC.targetUnit).to.eq(subjectNewComponentsTargetUnits[0]);
      expect(assetInfoDai.targetUnit).to.eq(subjectOldComponentsTargetUnits[0]);
    });

    it("should set the position multipler in the index module", async () => {
      await subject();
      const positionMultiplier = await indexModule.positionMultiplier();
      expect(positionMultiplier).to.eq(subjectPositionMultiplier);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#setTradeMaximums", async () => {
    let subjectComponents: Address[];
    let subjectTradeMaximums: BigNumber[];
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectComponents = [setV2Setup.usdc.address];
      subjectTradeMaximums = [BigNumber.from(1000000)];
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.setTradeMaximums(subjectComponents, subjectTradeMaximums);
    }

    it("should set the trade max in the index module", async () => {
      await subject();
      const assetInfo = await indexModule.assetInfo(subjectComponents[0]);
      expect(assetInfo.maxSize).to.eq(subjectTradeMaximums[0]);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#setAssetExchanges", async () => {
    let subjectComponents: Address[];
    let subjectAssetExchanges: BigNumber[];
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectComponents = [setV2Setup.usdc.address];
      subjectAssetExchanges = [BigNumber.from(1)];
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.setAssetExchanges(subjectComponents, subjectAssetExchanges);
    }

    it("should set the asset exchanges in the index module", async () => {
      await subject();
      const assetInfo = await indexModule.assetInfo(subjectComponents[0]);
      expect(assetInfo.exchange).to.eq(subjectAssetExchanges[0]);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#setCoolOffPeriods", async () => {
    let subjectComponents: Address[];
    let subjectCoolOffPeriods: BigNumber[];
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectComponents = [setV2Setup.usdc.address];
      subjectCoolOffPeriods = [BigNumber.from(100)];
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.setCoolOffPeriods(subjectComponents, subjectCoolOffPeriods);
    }

    it("should set the correct cool off period in the index module", async () => {
      await subject();
      const assetInfo = await indexModule.assetInfo(subjectComponents[0]);
      expect(assetInfo.coolOffPeriod).to.eq(subjectCoolOffPeriods[0]);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#updateTraderStatus", async () => {
    let subjectTraders: Address[];
    let subjectStatuses: boolean[];
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectTraders = [owner.address, trader.address];
      subjectStatuses = [true, true];
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateTraderStatus(subjectTraders, subjectStatuses);
    }

    it("should set the allowed traders in the index module", async () => {
      await subject();
      const isAllowedOne = await indexModule.tradeAllowList(subjectTraders[0]);
      const isAllowedTwo = await indexModule.tradeAllowList(subjectTraders[1]);
      expect(isAllowedOne).to.eq(true);
      expect(isAllowedTwo).to.eq(true);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#updateAnyoneTrade", async () => {
    let subjectStatus: boolean;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectStatus = true;
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateAnyoneTrade(subjectStatus);
    }

    it("should set the allowed traders in the index module", async () => {
      await subject();
      const isAllowed = await indexModule.anyoneTrade();
      expect(isAllowed).to.be.true;
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#accrueFeeAndDistribute", async () => {
    let subjectTimeFastForward: BigNumber;

    beforeEach(async () => {
      subjectTimeFastForward = ONE_YEAR_IN_SECONDS;
    });

    async function subject(): Promise<any> {
      await increaseTimeAsync(subjectTimeFastForward);
      return icManager.accrueFeeAndDistribute();
    }

    it("mints the correct amount of new Sets to the operator and methodologist", async () => {
      const feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
      const totalSupply = await setToken.totalSupply();

      const txnTimestamp = await getTransactionTimestamp(subject());

      const expectedFeeInflation = await getStreamingFee(
        setV2Setup.streamingFeeModule,
        setToken.address,
        feeState.lastStreamingFeeTimestamp,
        txnTimestamp
      );

      const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);
      const operatorFeeSplit = await icManager.operatorFeeSplit();
      const operatorTake = preciseMul(feeInflation, operatorFeeSplit);
      const methodologistTake = feeInflation.sub(operatorTake);
      const operatorSetBalance = await setToken.balanceOf(owner.address);
      const methodologistSetBalance = await setToken.balanceOf(methodologist.address);

      expect(operatorSetBalance).to.eq(operatorTake);
      expect(methodologistSetBalance).to.eq(methodologistTake);
    });

    it("should emit the correct FeesAccrued event", async () => {
      await expect(subject()).to.emit(icManager, "FeesAccrued");
    });
  });

  describe("#updateManager", async () => {
    let subjectNewManager: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewManager = newManager.address;
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateManager(subjectNewManager);
    }

    it("should log the proposed manager hash in the mutualUpgrades mapping", async () => {
      const txHash = await subject();

      const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]);
      const isLogged = await icManager.mutualUpgrades(expectedHash);

      expect(isLogged).to.be.true;
    });

    describe("when proposed manager hash is already set", async () => {
      beforeEach(async () => {
        await icManager.connect(owner.wallet).updateManager(newManager.address);

        subjectCaller = methodologist;
      });

      it("should change the manager address", async () => {
        await subject();
        const manager = await setToken.manager();

        expect(manager).to.eq(newManager.address);
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be authorized address");
      });
    });
  });

  describe("#addModule", async () => {
    let subjectModule: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      await setV2Setup.controller.addModule(otherAccount.address);

      subjectModule = otherAccount.address;
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.addModule(subjectModule);
    }

    it("should add the module to the SetToken", async () => {
      await subject();
      const isModule = await setToken.isPendingModule(subjectModule);
      expect(isModule).to.eq(true);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#interactModule", async () => {
    let subjectModule: Address;
    let subjectCallData: Bytes;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectModule = indexModule.address;

      // Invoke start rebalance
      subjectCallData = indexModule.interface.encodeFunctionData("startRebalance", [
        [setV2Setup.usdc.address],
        [BigNumber.from(500000)],
        [ether(.5)],
        ether(1),
      ]);
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.interactModule(subjectModule, subjectCallData);
    }

    it("should call startRebalance on the index module from the SetToken", async () => {
      await subject();
      const assetInfo = await indexModule.assetInfo(setV2Setup.dai.address);
      const positionMultiplier = await indexModule.positionMultiplier();
      expect(assetInfo.targetUnit).to.eq(ether(.5));
      expect(positionMultiplier).to.eq(ether(1));
    });

    describe("when interacting with the fee module", async () => {
      beforeEach(async () => {
        subjectModule = setV2Setup.streamingFeeModule.address;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must not be fee module");
      });
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#removeModule", async () => {
    let subjectModule: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectModule = setV2Setup.streamingFeeModule.address;
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.removeModule(subjectModule);
    }

    it("should remove the module from the SetToken", async () => {
      await subject();
      const isModule = await setToken.isInitializedModule(subjectModule);
      expect(isModule).to.eq(false);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#updateStreamingFee", async () => {
    let subjectStreamingFeePercentage: BigNumber;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectStreamingFeePercentage = ether(0.02);
      subjectCaller = methodologist;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateStreamingFee(subjectStreamingFeePercentage);
    }

    context("when no timelock period has been set", async () => {
      it("sets the new streaming fee", async () => {
        await subject();
        const feeStates = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        const newStreamingFee = feeStates.streamingFeePercentage;

        expect(newStreamingFee).to.eq(subjectStreamingFeePercentage);
      });

      describe("when the caller is not the methodologist", async () => {
        beforeEach(async () => {
          subjectCaller = await getRandomAccount();
        });

        it("should revert", async () => {
          await expect(subject()).to.be.revertedWith("Must be methodologist");
        });
      });
    });

    context("when 1 day timelock period has been set", async () => {
      beforeEach(async () => {
        icManager = icManager.connect(methodologist.wallet);
        await icManager.setTimeLockPeriod(ONE_DAY_IN_SECONDS);

        icManager = icManager.connect(owner.wallet);
        await icManager.setTimeLockPeriod(ONE_DAY_IN_SECONDS);
      });

      it("sets the upgradeHash", async () => {
        await subject();
        const timestamp = await getLastBlockTimestamp();
        const calldata = icManager.interface.encodeFunctionData("updateStreamingFee", [subjectStreamingFeePercentage]);
        const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
        const actualTimestamp = await icManager.timeLockedUpgrades(upgradeHash);
        expect(actualTimestamp).to.eq(timestamp);
      });

      context("when 1 day timelock has elapsed", async () => {
        beforeEach(async () => {
          await subject();
          await increaseTimeAsync(ONE_DAY_IN_SECONDS.add(1));
        });

        it("sets the new streaming fee", async () => {
          await subject();
          const feeStates = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
          const newStreamingFee = feeStates.streamingFeePercentage;

          expect(newStreamingFee).to.eq(subjectStreamingFeePercentage);
        });

        it("sets the upgradeHash to 0", async () => {
          await subject();
          const calldata = icManager.interface.encodeFunctionData("updateStreamingFee", [subjectStreamingFeePercentage]);
          const upgradeHash = solidityKeccak256(["bytes"], [calldata]);
          const actualTimestamp = await icManager.timeLockedUpgrades(upgradeHash);
          expect(actualTimestamp).to.eq(ZERO);
        });
      });
    });
  });

  describe("#updateFeeRecipient", async () => {
    let subjectNewFeeRecipient: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewFeeRecipient = otherAccount.address;
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateFeeRecipient(subjectNewFeeRecipient);
    }

    it("should log the proposed manager hash in the mutualUpgrades mapping", async () => {
      const txHash = await subject();

      const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]);
      const isLogged = await icManager.mutualUpgrades(expectedHash);

      expect(isLogged).to.be.true;
    });

    describe("when proposed recipient hash is already set", async () => {
      beforeEach(async () => {
        icManager = icManager.connect(owner.wallet);
        await icManager.updateFeeRecipient(otherAccount.address);

        subjectCaller = methodologist;
      });

      it("should change the recipient address", async () => {
        await subject();
        const feeStates = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        const feeRecipient = feeStates.feeRecipient;

        expect(feeRecipient).to.eq(otherAccount.address);
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be authorized address");
      });
    });
  });

  describe("#updateFeeSplit", async () => {
    let subjectNewFeeSplit: BigNumber;
    let subjectCaller: Account;

    beforeEach(async () => {
      await increaseTimeAsync(ONE_YEAR_IN_SECONDS);

      subjectNewFeeSplit = ether(0.5); // 50% to operator
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateFeeSplit(subjectNewFeeSplit);
    }

    it("should log the proposed manager hash in the mutualUpgrades mapping", async () => {
      const txHash = await subject();

      const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]);
      const isLogged = await icManager.mutualUpgrades(expectedHash);

      expect(isLogged).to.be.true;
    });

    describe("when proposed recipient hash is already set", async () => {
      beforeEach(async () => {
        icManager = icManager.connect(owner.wallet);
        await icManager.updateFeeSplit(ether(0.5));

        subjectCaller = methodologist;
      });

      it("should change the fee split", async () => {
        await subject();
        const feeSplit = await icManager.operatorFeeSplit();

        expect(feeSplit).to.eq(subjectNewFeeSplit);
      });

      it("should accrue fees", async () => {
        const feeState = await setV2Setup.streamingFeeModule.feeStates(setToken.address);
        const totalSupply = await setToken.totalSupply();
        // Get fee split prior to function call
        const operatorFeeSplit = await icManager.operatorFeeSplit();
        const txnTimestamp = await getTransactionTimestamp(subject());

        const expectedFeeInflation = await getStreamingFee(
          setV2Setup.streamingFeeModule,
          setToken.address,
          feeState.lastStreamingFeeTimestamp,
          txnTimestamp
        );

        const feeInflation = getStreamingFeeInflationAmount(expectedFeeInflation, totalSupply);
        const operatorTake = preciseMul(feeInflation, operatorFeeSplit);
        const methodologistTake = feeInflation.sub(operatorTake);
        const operatorSetBalance = await setToken.balanceOf(owner.address);
        const methodologistSetBalance = await setToken.balanceOf(methodologist.address);

        expect(operatorSetBalance).to.eq(operatorTake);
        expect(methodologistSetBalance).to.eq(methodologistTake);
      });
    });

    describe("when operator fee split is greater than 1e18", async () => {
      beforeEach(async () => {
        subjectNewFeeSplit = ether(1.1);
        await subject();
        subjectCaller = methodologist;
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Operator Fee Split must be less than 1e18");
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be authorized address");
      });
    });
  });

  describe("#updateIndexModule", async () => {
    let subjectIndexModule: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectIndexModule = await getRandomAddress();
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateIndexModule(subjectIndexModule);
    }

    it("should set the new index module", async () => {
      await subject();
      const actualIndexModule = await icManager.indexModule();
      expect(actualIndexModule).to.eq(subjectIndexModule);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#updateMethodologist", async () => {
    let subjectNewMethodologist: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewMethodologist = await getRandomAddress();
      subjectCaller = methodologist;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateMethodologist(subjectNewMethodologist);
    }

    it("should set the new methodologist", async () => {
      await subject();
      const actualIndexModule = await icManager.methodologist();
      expect(actualIndexModule).to.eq(subjectNewMethodologist);
    });

    describe("when the caller is not the methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be methodologist");
      });
    });
  });

  describe("#updateOperator", async () => {
    let subjectNewOperator: Address;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectNewOperator = await getRandomAddress();
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.updateOperator(subjectNewOperator);
    }

    it("should set the new operator", async () => {
      await subject();
      const actualIndexModule = await icManager.operator();
      expect(actualIndexModule).to.eq(subjectNewOperator);
    });

    describe("when the caller is not the operator", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be operator");
      });
    });
  });

  describe("#setTimeLockPeriod", async () => {
    let subjectTimeLockPeriod: BigNumber;
    let subjectCaller: Account;

    beforeEach(async () => {
      subjectTimeLockPeriod = ONE_DAY_IN_SECONDS;
      subjectCaller = owner;
    });

    async function subject(): Promise<any> {
      icManager = icManager.connect(subjectCaller.wallet);
      return icManager.setTimeLockPeriod(subjectTimeLockPeriod);
    }

    it("should log the proposed manager hash in the mutualUpgrades mapping", async () => {
      const txHash = await subject();

      const expectedHash = solidityKeccak256(["bytes", "address"], [txHash.data, subjectCaller.address]);
      const isLogged = await icManager.mutualUpgrades(expectedHash);

      expect(isLogged).to.be.true;
    });

    describe("when proposed timelock period hash is already set", async () => {
      beforeEach(async () => {
        icManager = icManager.connect(owner.wallet);
        await icManager.setTimeLockPeriod(ONE_DAY_IN_SECONDS);

        subjectCaller = methodologist;
      });

      it("should change the timelock period", async () => {
        await subject();
        const actualTimeLockPeriod = await icManager.timeLockPeriod();

        expect(actualTimeLockPeriod).to.eq(subjectTimeLockPeriod);
      });
    });

    describe("when the caller is not the operator or methodologist", async () => {
      beforeEach(async () => {
        subjectCaller = await getRandomAccount();
      });

      it("should revert", async () => {
        await expect(subject()).to.be.revertedWith("Must be authorized address");
      });
    });
  });
});
Example #15
Source File: HongbaoPanel.tsx    From dope-monorepo with GNU General Public License v3.0 4 votes vote down vote up
HongbaoPanel = () => {
  const hongbao = useHongbao();
  const { account } = useWeb3React();
  const router = useRouter();

  // if event.args.typ == 0
  //      PAPER reward. event.args.value is the numUnopenedEnvelopes
  // if event.args.typ == 1
  //      Item reward. event.args.value is the item id
  const [claimed, setClaimed] = useState<boolean>();
  const [isClaiming, setIsClaiming] = useState(false);

  const eligibleForAirdrop = Object.keys(config.airdrop).includes(account || '');
  const numUnopenedEnvelopes = useMemo(() => config.airdrop[account!], [account]);

  useEffect(() => {
    if (!eligibleForAirdrop) return;
    hongbao
      .claimed(
        Buffer.from(
          solidityKeccak256(['address', 'uint256'], [account, numUnopenedEnvelopes]).slice(2),
          'hex',
        ),
      )
      .then(setClaimed);
  }, [eligibleForAirdrop, hongbao, account, numUnopenedEnvelopes]);

  const claim = useCallback(async () => {
    try {
      setIsClaiming(true);
      const proof = getProof(account!, numUnopenedEnvelopes.toString());
      const tx = await hongbao.claim(numUnopenedEnvelopes, proof, {
        gasLimit: 200000 + 100000 * numUnopenedEnvelopes,
      });
      const receipt = await tx.wait(1);

      const opens = receipt.logs.reduce<{ typ: number; id: string }[]>((o, log, idx) => {
        if (idx % 2 == 0) return o;
        const event = hongbao.interface.parseLog(log) as unknown as OpenedEvent;
        // Set roll to item id
        return [...o, { typ: event.args.typ, id: event.args.id.toString() }];
      }, []);

      router.push(
        {
          pathname: '/lunar-new-year/mint-success',
          query: { items: JSON.stringify(opens) },
        },
        {
          pathname: '/lunar-new-year/mint-success',
        },
      );
    } finally {
      setIsClaiming(false);
    }
  }, [hongbao, account, numUnopenedEnvelopes, router]);

  return (
    <PanelContainer>
      {eligibleForAirdrop && (
        <>
          <PanelTitleHeader>
            {!claimed ? 'A gift for you' : 'All envelopes have been opened'}
          </PanelTitleHeader>
          <PanelBody>
            <Image
              src={`/images/lunar_new_year_2022/${
                claimed ? 'hongbao-with-bg.png' : 'hongbao_animated.gif'
              }`}
              alt="A surprise awaits you"
            />
          </PanelBody>
          <PanelFooter>
            {!claimed && (
              <Button variant="cny" onClick={claim} disabled>
                {!isClaiming && `Open ${numUnopenedEnvelopes} Envelopes`}
                {isClaiming && <SpinnerMessage text="Opening Envelopes…" />}
              </Button>
            )}
            {claimed && (
              <Link href="/inventory?section=Gear" passHref>
                <Button variant="cny">View your gifts</Button>
              </Link>
            )}
          </PanelFooter>
        </>
      )}
      {!eligibleForAirdrop && (
        <div
          css={css`
            flex: 5;
            height: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 32px;
            background-color: #eee;
            text-align: center;
          `}
        >
          <Image src="/images/icon/dope-smiley-sad.svg" width="72px" alt="Frowney Face" />
          <br />
          <p>The connected wallet is not eligible for this airdrop.</p>
          <p>Purchase a rare accessory mask below!</p>
        </div>
      )}
    </PanelContainer>
  );
}
Example #16
Source File: deposit.test.ts    From hubble-contracts with MIT License 4 votes vote down vote up
describe("DepositManager", async function() {
    let contracts: allContracts;
    let tokenID: number;
    let erc20: ERC20ValueFactory;
    beforeEach(async function() {
        const [signer] = await ethers.getSigners();
        contracts = await deployAll(signer, {
            ...TESTING_PARAMS,
            GENESIS_STATE_ROOT: constants.HashZero
        });
        const { exampleToken, tokenRegistry, depositManager } = contracts;
        tokenID = (await tokenRegistry.nextTokenID()).toNumber() - 1;
        erc20 = new ERC20ValueFactory(await exampleToken.decimals());
        const LARGE_AMOUNT_OF_TOKEN = erc20.fromHumanValue("1000000").l1Value;
        await exampleToken.approve(
            depositManager.address,
            LARGE_AMOUNT_OF_TOKEN
        );
    });
    it("fails if batchID is incorrect", async function() {
        const stateTree = StateTree.new(TESTING_PARAMS.MAX_DEPTH);
        const vacancyProof = stateTree.getVacancyProof(
            0,
            TESTING_PARAMS.MAX_DEPOSIT_SUBTREE_DEPTH
        );

        const commit = TransferCommitment.new(
            stateTree.root,
            await contracts.blsAccountRegistry.root()
        );
        const batch = commit.toBatch();

        const invalidBatchID = 1337;
        await assert.isRejected(
            contracts.rollup.submitDeposits(
                invalidBatchID,
                batch.proofCompressed(0),
                vacancyProof,
                { value: TESTING_PARAMS.STAKE_AMOUNT }
            ),
            /.*batchID does not match nextBatchID.*/
        );
    });
    it("should bypass transfer if amount = 0", async function() {
        const { depositManager, exampleToken } = contracts;
        const amount = erc20.fromHumanValue("0");

        const txDeposit = await depositManager.depositFor(
            0,
            amount.l1Value,
            tokenID
        );
        const [event] = await exampleToken.queryFilter(
            exampleToken.filters.Transfer(null, null, null),
            txDeposit.blockHash
        );
        assert.isUndefined(event);
    });
    it("should fail if l1Amount is not a multiple of l2Unit", async function() {
        const { depositManager } = contracts;
        const amount = erc20.fromHumanValue("10");

        await assert.isRejected(
            depositManager.depositFor(0, amount.l1Value.sub(1), tokenID),
            "l1Amount should be a multiple of l2Unit"
        );
    });
    it("should fail if token allowance less than deposit amount", async function() {
        const { depositManager } = contracts;
        const amount = erc20.fromHumanValue("1000001");

        await assert.isRejected(
            depositManager.depositFor(0, amount.l1Value, tokenID),
            "token allowance not approved"
        );
    });
    it("should allow depositing 3 leaves in a subtree and merging the first 2", async function() {
        const { depositManager } = contracts;
        const amount = erc20.fromHumanValue("10");
        const deposit0 = State.new(0, tokenID, amount.l2Value, 0);
        const deposit1 = State.new(1, tokenID, amount.l2Value, 0);
        const deposit2 = State.new(2, tokenID, amount.l2Value, 0);
        const pendingDeposit = solidityKeccak256(
            ["bytes", "bytes"],
            [deposit0.toStateLeaf(), deposit1.toStateLeaf()]
        );

        const txDeposit0 = await depositManager.depositFor(
            0,
            amount.l1Value,
            tokenID
        );
        console.log(
            "Deposit 0 transaction cost",
            (await txDeposit0.wait()).gasUsed.toNumber()
        );

        const [event0] = await depositManager.queryFilter(
            depositManager.filters.DepositQueued(),
            txDeposit0.blockHash
        );

        const event0State = State.fromDepositQueuedEvent(event0);
        assert.equal(event0State.hash(), deposit0.hash());
        assert.equal(event0.args.subtreeID.toNumber(), 1);
        assert.equal(event0.args.depositID.toNumber(), 0);

        const txDeposit1 = await depositManager.depositFor(
            1,
            amount.l1Value,
            tokenID
        );
        console.log(
            "Deposit 1 transaction cost",
            (await txDeposit1.wait()).gasUsed.toNumber()
        );
        const [event1] = await depositManager.queryFilter(
            depositManager.filters.DepositQueued(),
            txDeposit1.blockHash
        );

        const event1State = State.fromDepositQueuedEvent(event1);
        assert.equal(event1State.hash(), deposit1.hash());
        assert.equal(event1.args.subtreeID.toNumber(), 1);
        assert.equal(event1.args.depositID.toNumber(), 1);

        const [eventReady] = await depositManager.queryFilter(
            depositManager.filters.DepositSubTreeReady(),
            txDeposit1.blockHash
        );
        const subtreeRoot = eventReady.args?.subtreeRoot;
        assert.equal(subtreeRoot, pendingDeposit);

        // Make sure subtreeID increments correctly
        const txDeposit2 = await depositManager.depositFor(
            2,
            amount.l1Value,
            tokenID
        );
        console.log(
            "Deposit 2 transaction cost",
            (await txDeposit2.wait()).gasUsed.toNumber()
        );
        const [event2] = await depositManager.queryFilter(
            depositManager.filters.DepositQueued(),
            txDeposit2.blockHash
        );

        const event2State = State.fromDepositQueuedEvent(event2);
        assert.equal(event2State.hash(), deposit2.hash());
        assert.equal(event2.args.subtreeID.toNumber(), 2);
        assert.equal(event2.args.depositID.toNumber(), 0);
    });

    it("submit a deposit Batch to rollup", async function() {
        const { depositManager, rollup, blsAccountRegistry } = contracts;

        const stateTree = StateTree.new(TESTING_PARAMS.MAX_DEPTH);

        const vacancyProof = stateTree.getVacancyProof(
            0,
            TESTING_PARAMS.MAX_DEPOSIT_SUBTREE_DEPTH
        );

        const initialCommitment = TransferCommitment.new(
            stateTree.root,
            await blsAccountRegistry.root()
        );
        const initialBatch = initialCommitment.toBatch();
        const initialBatchID = 1;
        await initialBatch.submit(
            rollup,
            initialBatchID,
            TESTING_PARAMS.STAKE_AMOUNT
        );
        const amount = erc20.fromHumanValue("10");

        const stateLeaves = [];
        const nDeposits = 2 ** TESTING_PARAMS.MAX_DEPOSIT_SUBTREE_DEPTH;

        for (let i = 0; i < nDeposits; i++) {
            await depositManager.depositFor(i, amount.l1Value, tokenID);
            const state = State.new(i, tokenID, amount.l2Value, 0);
            stateLeaves.push(state.toStateLeaf());
        }
        const nextBatchID = 2;
        await rollup.submitDeposits(
            nextBatchID,
            initialBatch.proofCompressed(0),
            vacancyProof,
            { value: TESTING_PARAMS.STAKE_AMOUNT }
        );
        const batchID = Number(await rollup.nextBatchID()) - 1;
        const depositSubTreeRoot = await rollup.deposits(batchID);
        const root = MemoryTree.merklize(stateLeaves).root;
        assert.equal(depositSubTreeRoot, root);
    });
});