@ethersproject/bignumber#parseFixed TypeScript Examples

The following examples show how to use @ethersproject/bignumber#parseFixed. 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: BalancerTokenAdmin.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
INITIAL_RATE = parseFixed('145000', 18).div(WEEK)
Example #2
Source File: ChildChainGaugeTokenAdder.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('ChildChainGaugeTokenAdder', () => {
  let vault: Vault;
  let adaptor: Contract;

  let token: Token;
  let balToken: Token;

  let gauge: Contract;
  let streamer: Contract;

  let gaugeTokenAdder: Contract;

  let admin: SignerWithAddress, distributor: SignerWithAddress;

  before('setup signers', async () => {
    [, admin, distributor] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy token', async () => {
    vault = await Vault.create({ admin });
    if (!vault.authorizer) throw Error('Vault has no Authorizer');

    adaptor = await deploy('AuthorizerAdaptor', { args: [vault.address] });

    token = await Token.create({ symbol: 'BPT' });
    balToken = await Token.create({ symbol: 'BAL' });

    const gaugeImplementation = await deploy('RewardsOnlyGauge', {
      args: [balToken.address, vault.address, adaptor.address],
    });
    const streamerImplementation = await deploy('ChildChainStreamer', { args: [balToken.address, adaptor.address] });

    const factory = await deploy('ChildChainLiquidityGaugeFactory', {
      args: [gaugeImplementation.address, streamerImplementation.address],
    });

    await factory.create(token.address);

    gauge = await deployedAt('RewardsOnlyGauge', await factory.getPoolGauge(token.address));
    streamer = await deployedAt('ChildChainStreamer', await factory.getPoolStreamer(token.address));

    gaugeTokenAdder = await deploy('ChildChainGaugeTokenAdder', { args: [factory.address, adaptor.address] });
  });

  sharedBeforeEach('set up permissions', async () => {
    // Allow the ChildChainGaugeTokenAdder to call the relevant functions on the AuthorizerAdaptor.
    const addRewardRole = await actionId(adaptor, 'add_reward', streamer.interface);
    const setRewardsRole = await actionId(adaptor, 'set_rewards', gauge.interface);

    await vault.grantPermissionsGlobally([addRewardRole, setRewardsRole], gaugeTokenAdder);
  });

  describe('constructor', () => {
    it('sets the vault address', async () => {
      expect(await gaugeTokenAdder.getVault()).to.be.eq(vault.address);
    });

    it('uses the authorizer of the vault', async () => {
      expect(await gaugeTokenAdder.getAuthorizer()).to.equal(vault.authorizer?.address);
    });

    it('tracks authorizer changes in the vault', async () => {
      const action = await actionId(vault.instance, 'setAuthorizer');
      await vault.grantPermissionsGlobally([action], admin.address);

      await vault.instance.connect(admin).setAuthorizer(ANY_ADDRESS);

      expect(await gaugeTokenAdder.getAuthorizer()).to.equal(ANY_ADDRESS);
    });
  });

  describe('addTokenToGauge', () => {
    let newRewardToken: Token;

    sharedBeforeEach('deploy token', async () => {
      newRewardToken = await Token.create({ symbol: 'REWARD' });
    });

    sharedBeforeEach('grant permission to add new tokens', async () => {
      const addTokenToGaugeRole = await actionId(gaugeTokenAdder, 'addTokenToGauge');

      await vault.grantPermissionsGlobally([addTokenToGaugeRole], admin);
    });

    context('when interacting with a gauge from the expected factory', () => {
      it('adds the token to the streamer', async () => {
        const tx = await gaugeTokenAdder
          .connect(admin)
          .addTokenToGauge(gauge.address, newRewardToken.address, distributor.address);
        const receipt = await tx.wait();

        expectEvent.inIndirectReceipt(receipt, streamer.interface, 'RewardDistributorUpdated', {
          reward_token: newRewardToken.address,
          distributor: distributor.address,
        });
        expectEvent.inIndirectReceipt(receipt, streamer.interface, 'RewardDurationUpdated', {
          reward_token: newRewardToken.address,
          duration: WEEK,
        });
      });

      it('leaves the streamer and gauge in a consistent state', async () => {
        await gaugeTokenAdder
          .connect(admin)
          .addTokenToGauge(gauge.address, newRewardToken.address, distributor.address);

        const MAX_REWARDS = 8;
        for (let i = 0; i < MAX_REWARDS; ++i) {
          const streamerRewardToken = await streamer.reward_tokens(i);
          const gaugeRewardToken = await gauge.reward_tokens(i);

          expect(streamerRewardToken).to.be.eq(gaugeRewardToken);
        }
      });

      context('when next claiming from the gauge', () => {
        const rewardAmount = parseFixed('1', 18);

        sharedBeforeEach('start reward distribution of new token', async () => {
          await gaugeTokenAdder
            .connect(admin)
            .addTokenToGauge(gauge.address, newRewardToken.address, distributor.address);

          await newRewardToken.mint(streamer.address, rewardAmount);
          await streamer.connect(distributor).notify_reward_amount(newRewardToken.address);
          await advanceTime(DAY);
        });

        it('pulls the new token to the gauge', async () => {
          const expectedRewardAmount = rewardAmount.mul(DAY).div(WEEK);

          await expectBalanceChange(() => streamer.get_reward(), new TokenList([newRewardToken]), [
            { account: streamer, changes: { [newRewardToken.symbol]: ['near', expectedRewardAmount.mul(-1)] } },
            { account: gauge, changes: { [newRewardToken.symbol]: ['near', expectedRewardAmount] } },
          ]);
        });
      });

      context('when interacting with a gauge not from the expected factory', () => {
        it('reverts', async () => {
          await expect(
            gaugeTokenAdder.connect(admin).addTokenToGauge(ANY_ADDRESS, newRewardToken.address, distributor.address)
          ).to.be.revertedWith('Invalid gauge');
        });
      });

      context("when the gauge's streamer has been changed from the original", () => {
        sharedBeforeEach("change the gauge's streamer", async () => {
          const addTokenToGaugeRole = await actionId(adaptor, 'set_rewards', gauge.interface);
          await vault.grantPermissionsGlobally([addTokenToGaugeRole], admin);

          await adaptor
            .connect(admin)
            .performAction(
              gauge.address,
              gauge.interface.encodeFunctionData('set_rewards', [
                ZERO_ADDRESS,
                ZERO_BYTES32,
                [
                  balToken.address,
                  ZERO_ADDRESS,
                  ZERO_ADDRESS,
                  ZERO_ADDRESS,
                  ZERO_ADDRESS,
                  ZERO_ADDRESS,
                  ZERO_ADDRESS,
                  ZERO_ADDRESS,
                ],
              ])
            );
        });

        it('reverts', async () => {
          await expect(
            gaugeTokenAdder.connect(admin).addTokenToGauge(gauge.address, newRewardToken.address, distributor.address)
          ).to.be.revertedWith('Not original gauge streamer');
        });
      });
    });
  });
});
Example #3
Source File: ChildChainStreamer.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('ChildChainStreamer', () => {
  let vault: Vault;
  let adaptor: Contract;

  let token: Token;
  let balToken: Token;

  let gauge: Contract;
  let streamer: Contract;

  let admin: SignerWithAddress, distributor: SignerWithAddress, other: SignerWithAddress;

  before('setup signers', async () => {
    [, admin, distributor, other] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy token', async () => {
    vault = await Vault.create({ admin });
    if (!vault.authorizer) throw Error('Vault has no Authorizer');

    adaptor = await deploy('AuthorizerAdaptor', { args: [vault.address] });

    token = await Token.create({ symbol: 'BPT' });
    balToken = await Token.create({ symbol: 'BAL' });

    const gaugeImplementation = await deploy('RewardsOnlyGauge', {
      args: [balToken.address, vault.address, adaptor.address],
    });
    const streamerImplementation = await deploy('ChildChainStreamer', { args: [balToken.address, adaptor.address] });

    const factory = await deploy('ChildChainLiquidityGaugeFactory', {
      args: [gaugeImplementation.address, streamerImplementation.address],
    });

    await factory.create(token.address);

    gauge = await deployedAt('RewardsOnlyGauge', await factory.getPoolGauge(token.address));
    streamer = await deployedAt('ChildChainStreamer', await factory.getPoolStreamer(token.address));
  });

  describe('remove_reward', () => {
    sharedBeforeEach('send tokens to streamer', async () => {
      await balToken.mint(streamer, 100);

      const removeRewardRole = await actionId(adaptor, 'remove_reward', streamer.interface);
      await vault.grantPermissionsGlobally([removeRewardRole], admin);
    });

    it('allows tokens to be recovered', async () => {
      const tokenBalanceBefore = await balToken.balanceOf(streamer);
      const tx = await adaptor
        .connect(admin)
        .performAction(
          streamer.address,
          streamer.interface.encodeFunctionData('remove_reward', [balToken.address, other.address])
        );

      expectEvent.inIndirectReceipt(await tx.wait(), balToken.instance.interface, 'Transfer', {
        from: streamer.address,
        to: other.address,
        value: tokenBalanceBefore,
      });
    });
  });

  describe('claim_rewards', () => {
    const rewardAmount = parseFixed('1', 18);

    sharedBeforeEach('set up distributor on streamer', async () => {
      const setDistributorActionId = await actionId(adaptor, 'set_reward_distributor', streamer.interface);
      await vault.grantPermissionsGlobally([setDistributorActionId], admin);

      const calldata = streamer.interface.encodeFunctionData('set_reward_distributor', [
        balToken.address,
        distributor.address,
      ]);
      await adaptor.connect(admin).performAction(streamer.address, calldata);
    });

    function itUpdatesTimestamp() {
      it('updates last update time', async () => {
        const tx = await streamer.get_reward();
        const lastUpdateTime = await streamer.last_update_time();

        expect(lastUpdateTime).to.be.eq(await getReceiptTimestamp(tx.wait()));
      });
    }

    sharedBeforeEach('send tokens to streamer', async () => {
      await balToken.mint(streamer.address, rewardAmount);
    });

    context('before reward period', () => {
      it("doesn't transfer any tokens", async () => {
        const tx = await streamer.get_reward();
        const receipt = await tx.wait();

        expectEvent.notEmitted(receipt, 'Transfer');
      });

      itUpdatesTimestamp();
    });

    context('during reward period', () => {
      sharedBeforeEach('start reward period', async () => {
        await streamer.connect(distributor).notify_reward_amount(balToken.address);
        await advanceTime(DAY);
      });

      it('transfers the expected number of tokens', async () => {
        const expectedRewardAmount = rewardAmount.mul(DAY).div(WEEK);

        await expectBalanceChange(() => streamer.get_reward(), new TokenList([balToken]), [
          { account: streamer, changes: { BAL: ['near', expectedRewardAmount.mul(-1)] } },
          { account: gauge, changes: { BAL: ['near', expectedRewardAmount] } },
        ]);
      });

      itUpdatesTimestamp();
    });

    context('after reward period', () => {
      sharedBeforeEach('start reward period', async () => {
        await streamer.connect(distributor).notify_reward_amount(balToken.address);
        await advanceTime(WEEK);
      });

      it('transfers the expected number of tokens', async () => {
        // We need to account for rounding errors for rewards per second
        const expectedRewardAmount = rewardAmount.div(WEEK).mul(WEEK);
        await expectBalanceChange(() => streamer.get_reward(), new TokenList([balToken]), [
          { account: streamer, changes: { BAL: expectedRewardAmount.mul(-1) } },
          { account: gauge, changes: { BAL: expectedRewardAmount } },
        ]);
      });

      itUpdatesTimestamp();
    });
  });
});
Example #4
Source File: FeeDistributor.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('FeeDistributor', () => {
  let bpt: Token;
  let votingEscrow: Contract;
  let feeDistributor: Contract;

  let startTime: BigNumber;

  let user1: SignerWithAddress, user2: SignerWithAddress, other: SignerWithAddress;

  before('setup signers', async () => {
    [, user1, user2, other] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy fee distributor', async () => {
    bpt = await Token.create('BPT');
    votingEscrow = await deploy('VotingEscrow', {
      args: [bpt.address, 'Vote Escrowed Balancer BPT', 'veBAL', ANY_ADDRESS],
    });

    // startTime is rounded up to the beginning of next week
    startTime = roundUpTimestamp(await currentTimestamp());
    feeDistributor = await deploy('FeeDistributor', {
      args: [votingEscrow.address, startTime],
    });
  });

  sharedBeforeEach('lock BPT into VotingEscrow', async () => {
    const bptAmount = parseFixed('1', 18);
    await createLockForUser(user1, bptAmount, 365 * DAY);
    await createLockForUser(user2, bptAmount, 365 * DAY);

    expect(await votingEscrow['balanceOf(address)'](user1.address)).to.be.gt(0, 'zero veBAL balance');
    expect(await votingEscrow['totalSupply()']()).to.be.gt(0, 'zero veBAL supply');
  });

  async function createLockForUser(
    user: SignerWithAddress,
    amount: BigNumberish,
    lockDuration: BigNumberish
  ): Promise<void> {
    await bpt.mint(user, amount);
    await bpt.approve(votingEscrow, amount, { from: user });
    const now = await currentTimestamp();
    await votingEscrow.connect(user).create_lock(amount, now.add(lockDuration));
  }

  async function expectConsistentUserBalance(user: Account, timestamp: BigNumberish): Promise<void> {
    const userAddress = TypesConverter.toAddress(user);
    const cachedBalance = feeDistributor.getUserBalanceAtTimestamp(userAddress, timestamp);
    const expectedBalance = votingEscrow['balanceOf(address,uint256)'](userAddress, timestamp);
    expect(await cachedBalance).to.be.eq(await expectedBalance);
  }

  async function expectConsistentTotalSupply(timestamp: BigNumberish): Promise<void> {
    const cachedSupply = feeDistributor.getTotalSupplyAtTimestamp(timestamp);
    const expectedSupply = votingEscrow['totalSupply(uint256)'](timestamp);
    expect(await cachedSupply).to.be.eq(await expectedSupply);
  }

  describe('constructor', () => {
    it('sets the VotingEscrow contract address', async () => {
      expect(await feeDistributor.getVotingEscrow()).to.be.eq(votingEscrow.address);
    });

    it('sets the time cursor to the expected value', async () => {
      expectTimestampsMatch(await feeDistributor.getTimeCursor(), startTime);
    });
  });

  describe('checkpointing', () => {
    describe('global checkpoint', () => {
      context('when startTime has not passed', () => {
        it('does nothing', async () => {
          expectTimestampsMatch(await feeDistributor.getTimeCursor(), startTime);
          expect(await feeDistributor.getTotalSupplyAtTimestamp(startTime)).to.be.eq(0);

          await feeDistributor.checkpoint();

          expectTimestampsMatch(await feeDistributor.getTimeCursor(), startTime);
          expect(await feeDistributor.getTotalSupplyAtTimestamp(startTime)).to.be.eq(0);
        });
      });

      context('when startTime has passed', () => {
        sharedBeforeEach('advance time past startTime', async () => {
          await advanceToTimestamp(startTime);
        });

        context('when the contract has already been checkpointed', () => {
          let nextWeek: BigNumber;

          sharedBeforeEach('checkpoint contract', async () => {
            // We checkpoint the contract so that the next time
            // we call this function there will be no update to perform.
            const tx = await feeDistributor.checkpoint();
            nextWeek = roundUpTimestamp(await getReceiptTimestamp(tx.wait()));
          });

          it('nothing happens', async () => {
            expectTimestampsMatch(await feeDistributor.getTimeCursor(), nextWeek);

            await feeDistributor.checkpoint();

            expectTimestampsMatch(await feeDistributor.getTimeCursor(), nextWeek);
          });
        });

        context('when the contract has not been checkpointed this week', () => {
          let start: BigNumber;
          let end: BigNumber;

          sharedBeforeEach('advance time past startTime', async () => {
            start = roundUpTimestamp(await currentTimestamp());
          });

          function testCheckpoint() {
            let numWeeks: number;
            let checkpointTimestamps: BigNumber[];

            sharedBeforeEach('advance time to end of period to checkpoint', async () => {
              numWeeks = roundDownTimestamp(end).sub(roundDownTimestamp(start)).div(WEEK).toNumber();
              checkpointTimestamps = Array.from({ length: numWeeks }, (_, i) =>
                roundDownTimestamp(start).add(i * WEEK)
              );
              await advanceToTimestamp(end);
            });

            it('advances the global time cursor to the start of the next week', async () => {
              expectTimestampsMatch(await feeDistributor.getTimeCursor(), start);

              const tx = await feeDistributor.checkpoint();

              const txTimestamp = await getReceiptTimestamp(tx.wait());
              // Add 1 as if the transaction falls exactly on the beginning of the week
              // then we also go to the end of the week as we can read the current balance
              const nextWeek = roundUpTimestamp(txTimestamp + 1);

              expectTimestampsMatch(await feeDistributor.getTimeCursor(), nextWeek);
            });

            it('stores the VotingEscrow supply at the start of each week', async () => {
              for (let i = 0; i < numWeeks; i++) {
                expect(await feeDistributor.getTotalSupplyAtTimestamp(checkpointTimestamps[i])).to.be.eq(0);
              }

              await feeDistributor.checkpoint();

              for (let i = 0; i < numWeeks; i++) {
                await expectConsistentTotalSupply(checkpointTimestamps[i]);
              }
            });
          }

          context("when the contract hasn't checkpointed in a small number of weeks", () => {
            sharedBeforeEach('set end timestamp', async () => {
              end = start.add(8 * WEEK - 1);
            });
            testCheckpoint();
          });
        });
      });
    });

    describe('user checkpoint', () => {
      describe('checkpointUser', () => {
        context('when startTime has not passed', () => {
          it('does not advance the user time cursor past startTime', async () => {
            expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user1.address), 0);

            await feeDistributor.checkpointUser(user1.address);

            expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user1.address), startTime);

            await feeDistributor.checkpointUser(user1.address);

            expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user1.address), startTime);
          });

          it("does not write a value for user's balance at startTime", async () => {
            expectTimestampsMatch(await feeDistributor.getUserBalanceAtTimestamp(user1.address, startTime), 0);

            await feeDistributor.checkpointUser(user1.address);

            expectTimestampsMatch(await feeDistributor.getUserBalanceAtTimestamp(user1.address, startTime), 0);
          });
        });

        context('when startTime has passed', () => {
          context('when the user has already been checkpointed', () => {
            let nextWeek: BigNumber;

            sharedBeforeEach('checkpoint contract', async () => {
              // We checkpoint the contract so that the next time
              // we call this function there will be no update to perform.
              const tx = await feeDistributor.checkpointUser(user1.address);
              nextWeek = roundUpTimestamp(await getReceiptTimestamp(tx.wait()));
            });

            it('nothing happens', async () => {
              expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user1.address), nextWeek);

              await feeDistributor.checkpointUser(user1.address);

              expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user1.address), nextWeek);
            });
          });

          context('when the user has not been checkpointed this week', () => {
            let user: SignerWithAddress;
            let start: BigNumber;
            let end: BigNumber;

            sharedBeforeEach('advance time past startTime', async () => {
              await advanceToTimestamp(startTime.add(1));

              start = await currentTimestamp();
            });

            function testCheckpoint() {
              // These tests will begin to fail as we increase the number of weeks which we are checkpointing
              // This is as `_checkpointUserBalance` is limited to perform at most 50 iterations minus the number
              // of user epochs in the period being checkpointed.
              let numWeeks: number;
              let checkpointTimestamps: BigNumber[];

              sharedBeforeEach('advance time to end of period to checkpoint', async () => {
                numWeeks = roundDownTimestamp(end).sub(roundDownTimestamp(start)).div(WEEK).toNumber();
                checkpointTimestamps = Array.from({ length: numWeeks }, (_, i) =>
                  roundDownTimestamp(start).add(i * WEEK)
                );
                await advanceToTimestamp(end);
              });

              it("advances the user's time cursor to the start of the next week", async () => {
                expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user.address), 0);

                const tx = await feeDistributor.checkpointUser(user.address);

                const txTimestamp = await getReceiptTimestamp(tx.wait());
                // Add 1 as if the transaction falls exactly on the beginning of the week
                // then we also go to the end of the week as we can read the current balance
                const nextWeek = roundUpTimestamp(txTimestamp + 1);

                expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user.address), nextWeek);
              });

              it("stores the user's balance at the start of each week", async () => {
                for (let i = 0; i < numWeeks; i++) {
                  expect(
                    await feeDistributor.getUserBalanceAtTimestamp(user.address, checkpointTimestamps[i])
                  ).to.be.eq(0);
                }

                await feeDistributor.checkpointUser(user.address);

                for (let i = 0; i < numWeeks; i++) {
                  await expectConsistentUserBalance(user, checkpointTimestamps[i]);
                }
              });
            }

            context("when user hasn't checkpointed in a small number of weeks", () => {
              sharedBeforeEach('set end timestamp', async () => {
                end = start.add(8 * WEEK - 1);
              });
              context('when user locked prior to the beginning of the week', () => {
                sharedBeforeEach('set user', async () => {
                  user = user1;
                });
                testCheckpoint();
              });

              context('when user locked after the beginning of the week', () => {
                sharedBeforeEach('set user', async () => {
                  user = other;
                  await createLockForUser(other, parseFixed('1', 18), 365 * DAY);
                });
                testCheckpoint();

                it('records a zero balance for the week in which they lock', async () => {
                  expect(await feeDistributor.getUserBalanceAtTimestamp(user.address, startTime)).to.be.eq(0);

                  await feeDistributor.checkpointUser(user.address);

                  await expectConsistentUserBalance(user, startTime);
                });
              });
            });
          });
        });
      });
    });

    describe('token checkpoint', () => {
      let tokens: TokenList;
      let tokenAmounts: BigNumber[];

      function itCheckpointsTokensCorrectly(checkpointTokens: () => Promise<ContractTransaction>): void {
        context('when startTime has not passed', () => {
          it('reverts', async () => {
            await expect(checkpointTokens()).to.be.revertedWith('Fee distribution has not started yet');
          });
        });

        context('when startTime has passed', () => {
          sharedBeforeEach('advance time past startTime', async () => {
            await advanceToTimestamp(startTime.add(100));
          });

          it("updates the token's time cursor to the current timestamp", async () => {
            const tx = await checkpointTokens();

            for (const token of tokens.addresses) {
              const tokenTimeCursor = await feeDistributor.getTokenTimeCursor(token);
              const txTimestamp = await getReceiptTimestamp(tx.wait());
              expectTimestampsMatch(tokenTimeCursor, txTimestamp);
            }
          });

          context("when FeeDistributor hasn't received new tokens", () => {
            sharedBeforeEach('send tokens and checkpoint', async () => {
              for (const [index, token] of tokens.tokens.entries()) {
                await token.mint(feeDistributor, tokenAmounts[index]);
              }
              await feeDistributor.checkpointTokens(tokens.addresses);
            });

            it('maintains the same cached balance', async () => {
              const expectedTokenLastBalances = await Promise.all(
                tokens.addresses.map((token) => feeDistributor.getTokenLastBalance(token))
              );
              await checkpointTokens();

              for (const [index, token] of tokens.addresses.entries()) {
                expect(await feeDistributor.getTokenLastBalance(token)).to.be.eq(expectedTokenLastBalances[index]);
              }
            });
          });

          context('when FeeDistributor has received new tokens', () => {
            sharedBeforeEach('send tokens', async () => {
              for (const [index, token] of tokens.tokens.entries()) {
                await token.mint(feeDistributor, tokenAmounts[index]);
              }
            });

            it('emits a TokenCheckpointedEvent', async () => {
              const tx = await checkpointTokens();

              for (const [index, token] of tokens.tokens.entries()) {
                expectEvent.inReceipt(await tx.wait(), 'TokenCheckpointed', {
                  token: token.address,
                  amount: tokenAmounts[index],
                });
              }
            });

            it('updates the cached balance by the amount of new tokens received', async () => {
              const previousTokenLastBalances = await Promise.all(
                tokens.addresses.map((token) => feeDistributor.getTokenLastBalance(token))
              );
              await checkpointTokens();
              const newTokenLastBalances = await Promise.all(
                tokens.addresses.map((token) => feeDistributor.getTokenLastBalance(token))
              );

              for (const index in tokens.addresses) {
                expect(newTokenLastBalances[index].sub(previousTokenLastBalances[index])).to.be.eq(tokenAmounts[index]);
              }
            });
          });
        });
      }

      describe('checkpointToken', () => {
        sharedBeforeEach('Deploy protocol fee token', async () => {
          tokens = await TokenList.create(['FEE']);
          tokenAmounts = tokens.map(() => parseFixed('1', 18));
        });

        itCheckpointsTokensCorrectly(() => feeDistributor.checkpointToken(tokens.addresses[0]));
      });

      describe('checkpointTokens', () => {
        sharedBeforeEach('Deploy protocol fee token', async () => {
          tokens = await TokenList.create(['FEE', 'FEE2']);
          tokenAmounts = tokens.map(() => parseFixed('1', 18));
        });

        itCheckpointsTokensCorrectly(() => feeDistributor.checkpointTokens(tokens.addresses));
      });
    });
  });

  describe('claiming', () => {
    let tokens: TokenList;
    let tokenAmounts: BigNumber[];

    function itClaimsTokensCorrectly(
      claimTokens: () => Promise<ContractTransaction>,
      simulateClaimTokens: () => Promise<BigNumber[]>
    ): void {
      context('when startTime has not passed', () => {
        it('reverts', async () => {
          await expect(claimTokens()).to.be.revertedWith('Fee distribution has not started yet');
        });
      });

      context('when startTime has passed', () => {
        sharedBeforeEach('advance time past startTime', async () => {
          await advanceToTimestamp(startTime.add(100));
        });

        it('checkpoints the global, token and user state', async () => {
          const nextWeek = roundUpTimestamp(await currentTimestamp());
          const tx = await claimTokens();

          // Global
          expectTimestampsMatch(await feeDistributor.getTimeCursor(), nextWeek);

          // Token
          // This only works as it is the first token checkpoint. Calls for the next day won't checkpoint
          const txTimestamp = await getReceiptTimestamp(tx.wait());
          for (const token of tokens.addresses) {
            const tokenTimeCursor = await feeDistributor.getTokenTimeCursor(token);
            expectTimestampsMatch(tokenTimeCursor, txTimestamp);
          }

          // User
          expectTimestampsMatch(await feeDistributor.getUserTimeCursor(user1.address), nextWeek);
        });

        it('updates the token time cursor for the user to the latest claimed week', async () => {
          const thisWeek = roundDownTimestamp(await currentTimestamp());

          await claimTokens();
          for (const token of tokens.addresses) {
            expectTimestampsMatch(await feeDistributor.getUserTokenTimeCursor(user1.address, token), thisWeek);
          }
        });

        context('when there are no tokens to distribute to user', () => {
          it("doesn't emit a TokensClaimed event", async () => {
            const tx = await claimTokens();
            expectEvent.notEmitted(await tx.wait(), 'TokensClaimed');
          });

          it('maintains the same cached balance', async () => {
            const expectedTokenLastBalances = await Promise.all(
              tokens.addresses.map((token) => feeDistributor.getTokenLastBalance(token))
            );
            await claimTokens();

            for (const [index, token] of tokens.addresses.entries()) {
              expect(await feeDistributor.getTokenLastBalance(token)).to.be.eq(expectedTokenLastBalances[index]);
            }
          });

          it('returns zero', async () => {
            expect(await simulateClaimTokens()).to.be.eql(tokenAmounts.map(() => BigNumber.from(0)));
          });
        });

        context('when there are tokens to distribute to user', () => {
          sharedBeforeEach('send tokens', async () => {
            for (const [index, token] of tokens.tokens.entries()) {
              await token.mint(feeDistributor, tokenAmounts[index]);
            }
            await feeDistributor.checkpointTokens(tokens.addresses);

            // For the week to become claimable we must wait until the next week starts
            await advanceToNextWeek();
          });

          it('emits a TokensClaimed event', async () => {
            const thisWeek = roundDownTimestamp(await currentTimestamp());

            const tx = await claimTokens();
            for (const [index, token] of tokens.tokens.entries()) {
              expectEvent.inReceipt(await tx.wait(), 'TokensClaimed', {
                user: user1.address,
                token: token.address,
                amount: tokenAmounts[index].div(2),
                userTokenTimeCursor: thisWeek,
              });
            }
          });

          it('subtracts the number of tokens claimed from the cached balance', async () => {
            const previousTokenLastBalances = await Promise.all(
              tokens.addresses.map((token) => feeDistributor.getTokenLastBalance(token))
            );
            const tx = await claimTokens();
            const newTokenLastBalances = await Promise.all(
              tokens.addresses.map((token) => feeDistributor.getTokenLastBalance(token))
            );

            for (const [index, token] of tokens.tokens.entries()) {
              const {
                args: { amount },
              } = expectEvent.inReceipt(await tx.wait(), 'TokensClaimed', {
                user: user1.address,
                token: token.address,
              });
              expect(newTokenLastBalances[index]).to.be.eq(previousTokenLastBalances[index].sub(amount));
            }
          });

          it('returns the amount of tokens claimed', async () => {
            expect(await simulateClaimTokens()).to.be.eql(tokenAmounts.map((amount) => amount.div(2)));
          });
        });
      });
    }

    describe('claimToken', () => {
      sharedBeforeEach('Deploy protocol fee token', async () => {
        tokens = await TokenList.create(['FEE']);
        tokenAmounts = tokens.map(() => parseFixed('1', 18));
      });

      // Return values from static-calling claimToken need to be converted into array format to standardise test code.
      itClaimsTokensCorrectly(
        () => feeDistributor.claimToken(user1.address, tokens.addresses[0]),
        async () => [await feeDistributor.callStatic.claimToken(user1.address, tokens.addresses[0])]
      );
    });

    describe('claimTokens', () => {
      sharedBeforeEach('Deploy protocol fee token', async () => {
        tokens = await TokenList.create(['FEE', 'FEE2']);
        tokenAmounts = tokens.map(() => parseFixed('1', 18));
      });

      itClaimsTokensCorrectly(
        () => feeDistributor.claimTokens(user1.address, tokens.addresses),
        () => feeDistributor.callStatic.claimTokens(user1.address, tokens.addresses)
      );

      context('when startTime has passed', () => {
        sharedBeforeEach('advance time past startTime', async () => {
          await advanceToTimestamp(startTime.add(100));
        });

        context('when there are tokens to distribute to user', () => {
          sharedBeforeEach('send tokens', async () => {
            for (const [index, token] of tokens.tokens.entries()) {
              await token.mint(feeDistributor, tokenAmounts[index]);
            }
            await feeDistributor.checkpointTokens(tokens.addresses);

            // For the week to become claimable we must wait until the next week starts
            await advanceToNextWeek();
          });

          context('when the array of tokens contains duplicates', () => {
            it('ignores the second occurence of the token address', async () => {
              expect(await feeDistributor.callStatic.claimTokens(user1.address, tokens.addresses)).to.be.eql(
                tokenAmounts.map((amount) => amount.div(2))
              );
            });
          });
        });
      });
    });
  });
});
Example #5
Source File: RewardsOnlyGauge.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('RewardsOnlyGauge', () => {
  let vault: Vault;
  let adaptor: Contract;

  let token: Token;
  let balToken: Token;

  let gauge: Contract;
  let streamer: Contract;

  let admin: SignerWithAddress,
    distributor: SignerWithAddress,
    holder: SignerWithAddress,
    spender: SignerWithAddress,
    other: SignerWithAddress;

  before('setup signers', async () => {
    [, admin, distributor, holder, spender, other] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy token', async () => {
    vault = await Vault.create({ admin });
    if (!vault.authorizer) throw Error('Vault has no Authorizer');

    adaptor = await deploy('AuthorizerAdaptor', { args: [vault.address] });

    token = await Token.create({ symbol: 'BPT' });
    balToken = await Token.create({ symbol: 'BAL' });

    const gaugeImplementation = await deploy('RewardsOnlyGauge', {
      args: [balToken.address, vault.address, adaptor.address],
    });
    const streamerImplementation = await deploy('ChildChainStreamer', { args: [balToken.address, adaptor.address] });

    const factory = await deploy('ChildChainLiquidityGaugeFactory', {
      args: [gaugeImplementation.address, streamerImplementation.address],
    });

    await factory.create(token.address);

    gauge = await deployedAt('RewardsOnlyGauge', await factory.getPoolGauge(token.address));
    streamer = await deployedAt('ChildChainStreamer', await factory.getPoolStreamer(token.address));
  });

  describe('info', () => {
    it('setups the name properly', async () => {
      expect(await gauge.name()).to.be.equal(`Balancer ${token.symbol} RewardGauge Deposit`);
    });
  });

  describe('permit', () => {
    it('initial nonce is zero', async () => {
      expect(await gauge.nonces(holder.address)).to.equal(0);
    });

    const amount = bn(42);

    it('accepts holder signature', async function () {
      const previousNonce = await gauge.nonces(holder.address);
      const { v, r, s } = await signPermit(gauge, holder, spender, amount);

      const receipt = await (await gauge.permit(holder.address, spender.address, amount, MAX_DEADLINE, v, r, s)).wait();
      expectEvent.inReceipt(receipt, 'Approval', { _owner: holder.address, _spender: spender.address, _value: amount });

      expect(await gauge.nonces(holder.address)).to.equal(previousNonce.add(1));
      expect(await gauge.allowance(holder.address, spender.address)).to.equal(amount);
    });

    context('with invalid signature', () => {
      let v: number, r: string, s: string, deadline: BigNumber;

      context('with reused signature', () => {
        beforeEach(async () => {
          ({ v, r, s, deadline } = await signPermit(gauge, holder, spender, amount));
          await gauge.permit(holder.address, spender.address, amount, deadline, v, r, s);
        });

        itReverts();
      });

      context('with signature for other holder', () => {
        beforeEach(async () => {
          ({ v, r, s, deadline } = await signPermit(gauge, spender, spender, amount));
        });

        itReverts();
      });

      context('with signature for other spender', () => {
        beforeEach(async () => {
          ({ v, r, s, deadline } = await signPermit(gauge, holder, holder, amount));
        });

        itReverts();
      });

      context('with signature for other amount', () => {
        beforeEach(async () => {
          ({ v, r, s, deadline } = await signPermit(gauge, holder, spender, amount.add(1)));
        });

        itReverts();
      });

      context('with signature for other token', () => {
        beforeEach(async () => {
          const otherToken = await Token.create('TKN');

          ({ v, r, s, deadline } = await signPermit(otherToken.instance, holder, spender, amount));
        });

        itReverts();
      });

      context('with signature with invalid nonce', () => {
        beforeEach(async () => {
          const currentNonce = await gauge.nonces(holder.address);
          ({ v, r, s, deadline } = await signPermit(gauge, holder, spender, amount, MAX_DEADLINE, currentNonce.add(1)));
        });

        itReverts();
      });

      context('with expired deadline', () => {
        beforeEach(async () => {
          const now = await currentTimestamp();

          ({ v, r, s, deadline } = await signPermit(gauge, holder, spender, amount, now.sub(1)));
        });

        itReverts();
      });

      function itReverts() {
        it('reverts', async () => {
          await expect(gauge.permit(holder.address, spender.address, amount, deadline, v, r, s)).to.be.reverted;
        });
      }
    });
  });

  describe('claim_rewards', () => {
    const rewardAmount = parseFixed('1', 18);

    sharedBeforeEach('stake into gauge', async () => {
      await token.mint(holder);
      await token.approve(gauge, MAX_UINT256, { from: holder });

      await gauge.connect(holder)['deposit(uint256)'](rewardAmount);
      await gauge.connect(holder)['deposit(uint256,address)'](rewardAmount.mul(2), other.address);
    });

    sharedBeforeEach('set up distributor on streamer', async () => {
      const setDistributorActionId = await actionId(adaptor, 'set_reward_distributor', streamer.interface);
      await vault.grantPermissionsGlobally([setDistributorActionId], admin);

      const calldata = streamer.interface.encodeFunctionData('set_reward_distributor', [
        balToken.address,
        distributor.address,
      ]);
      await adaptor.connect(admin).performAction(streamer.address, calldata);
    });

    sharedBeforeEach('send tokens to streamer', async () => {
      await balToken.mint(streamer.address, rewardAmount);
      await streamer.connect(distributor).notify_reward_amount(balToken.address);
    });

    context('during reward period', () => {
      sharedBeforeEach('start reward period', async () => {
        await advanceTime(DAY);
      });

      it('transfers the expected number of tokens', async () => {
        const expectedClaimAmount = rewardAmount.mul(DAY).div(WEEK).div(3);
        await expectBalanceChange(() => gauge.connect(holder)['claim_rewards()'](), new TokenList([balToken]), [
          { account: holder, changes: { BAL: ['near', expectedClaimAmount] } },
        ]);
      });
    });

    context('after reward period', () => {
      sharedBeforeEach('start reward period', async () => {
        await advanceTime(WEEK);
      });

      it('transfers the expected number of tokens', async () => {
        // We need to account for rounding errors for rewards per second
        const expectedClaimAmount = rewardAmount.div(WEEK).mul(WEEK).div(3);
        await expectBalanceChange(() => gauge.connect(holder)['claim_rewards()'](), new TokenList([balToken]), [
          { account: holder, changes: { BAL: expectedClaimAmount } },
        ]);
      });
    });
  });
});