UNPKG

@hyperlane-xyz/core

Version:

Core solidity contracts for Hyperlane

696 lines (638 loc) 24.1 kB
import { ethers } from 'hardhat' import { assert, expect } from 'chai' import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { ReceiveEmitter } from '../../../typechain/ReceiveEmitter' import { ReceiveFallbackEmitter } from '../../../typechain/ReceiveFallbackEmitter' import * as h from '../../test-helpers/helpers' import { ERC20BalanceMonitorExposed, LinkToken } from '../../../typechain' import { BigNumber } from 'ethers' const OWNABLE_ERR = 'Only callable by owner' const INVALID_WATCHLIST_ERR = `InvalidWatchList` const PAUSED_ERR = 'Pausable: paused' const ONLY_KEEPER_ERR = `OnlyKeeperRegistry` const zeroLINK = ethers.utils.parseEther('0') const oneLINK = ethers.utils.parseEther('1') const twoLINK = ethers.utils.parseEther('2') const threeLINK = ethers.utils.parseEther('3') const fiveLINK = ethers.utils.parseEther('5') const sixLINK = ethers.utils.parseEther('6') const tenLINK = ethers.utils.parseEther('10') const oneHundredLINK = ethers.utils.parseEther('100') const watchAddress1 = ethers.Wallet.createRandom().address const watchAddress2 = ethers.Wallet.createRandom().address const watchAddress3 = ethers.Wallet.createRandom().address const watchAddress4 = ethers.Wallet.createRandom().address let watchAddress5: string let watchAddress6: string let bm: ERC20BalanceMonitorExposed let lt: LinkToken let receiveEmitter: ReceiveEmitter let receiveFallbackEmitter: ReceiveFallbackEmitter let owner: SignerWithAddress let stranger: SignerWithAddress let keeperRegistry: SignerWithAddress async function assertWatchlistBalances( balance1: BigNumber, balance2: BigNumber, balance3: BigNumber, balance4: BigNumber, balance5: BigNumber, balance6: BigNumber, ) { await h.assertLinkTokenBalance(lt, watchAddress1, balance1, 'address 1') await h.assertLinkTokenBalance(lt, watchAddress2, balance2, 'address 2') await h.assertLinkTokenBalance(lt, watchAddress3, balance3, 'address 3') await h.assertLinkTokenBalance(lt, watchAddress4, balance4, 'address 4') await h.assertLinkTokenBalance(lt, watchAddress5, balance5, 'address 5') await h.assertLinkTokenBalance(lt, watchAddress6, balance6, 'address 6') } describe('ERC20BalanceMonitor', () => { beforeEach(async () => { const accounts = await ethers.getSigners() owner = accounts[0] stranger = accounts[1] keeperRegistry = accounts[2] watchAddress5 = accounts[3].address watchAddress6 = accounts[4].address const bmFactory = await ethers.getContractFactory( 'ERC20BalanceMonitorExposed', owner, ) const ltFactory = await ethers.getContractFactory( 'src/v0.8/shared/test/helpers/LinkTokenTestHelper.sol:LinkTokenTestHelper', owner, ) const reFactory = await ethers.getContractFactory('ReceiveEmitter', owner) const rfeFactory = await ethers.getContractFactory( 'ReceiveFallbackEmitter', owner, ) lt = await ltFactory.deploy() bm = await bmFactory.deploy(lt.address, keeperRegistry.address, 0) for (let i = 1; i <= 4; i++) { const recipient = await accounts[i].getAddress() await lt.connect(owner).transfer(recipient, oneHundredLINK) } receiveEmitter = await reFactory.deploy() receiveFallbackEmitter = await rfeFactory.deploy() await Promise.all([ bm.deployed(), receiveEmitter.deployed(), receiveFallbackEmitter.deployed(), ]) }) afterEach(async () => { await h.reset() }) describe('add funds', () => { it('Should allow anyone to add funds', async () => { await lt.transfer(bm.address, oneLINK) await lt.connect(stranger).transfer(bm.address, oneLINK) }) }) describe('withdraw()', () => { beforeEach(async () => { const tx = await lt.connect(owner).transfer(bm.address, oneLINK) await tx.wait() }) it('Should allow the owner to withdraw', async () => { const beforeBalance = await lt.balanceOf(owner.address) const tx = await bm.connect(owner).withdraw(oneLINK, owner.address) await tx.wait() const afterBalance = await lt.balanceOf(owner.address) assert.isTrue( afterBalance.gt(beforeBalance), 'balance did not increase after withdraw', ) }) it('Should emit an event', async () => { const tx = await bm.connect(owner).withdraw(oneLINK, owner.address) await expect(tx) .to.emit(bm, 'FundsWithdrawn') .withArgs(oneLINK, owner.address) }) it('Should allow the owner to withdraw to anyone', async () => { const beforeBalance = await lt.balanceOf(stranger.address) const tx = await bm.connect(owner).withdraw(oneLINK, stranger.address) await tx.wait() const afterBalance = await lt.balanceOf(stranger.address) assert.isTrue( beforeBalance.add(oneLINK).eq(afterBalance), 'balance did not increase after withdraw', ) }) it('Should not allow strangers to withdraw', async () => { const tx = bm.connect(stranger).withdraw(oneLINK, owner.address) await expect(tx).to.be.revertedWith(OWNABLE_ERR) }) }) describe('pause() / unpause()', () => { it('Should allow owner to pause / unpause', async () => { const pauseTx = await bm.connect(owner).pause() await pauseTx.wait() const unpauseTx = await bm.connect(owner).unpause() await unpauseTx.wait() }) it('Should not allow strangers to pause / unpause', async () => { const pauseTxStranger = bm.connect(stranger).pause() await expect(pauseTxStranger).to.be.revertedWith(OWNABLE_ERR) const pauseTxOwner = await bm.connect(owner).pause() await pauseTxOwner.wait() const unpauseTxStranger = bm.connect(stranger).unpause() await expect(unpauseTxStranger).to.be.revertedWith(OWNABLE_ERR) }) }) describe('setWatchList() / getWatchList() / getAccountInfo()', () => { it('Should allow owner to set the watchlist', async () => { // should start unactive assert.isFalse((await bm.getAccountInfo(watchAddress1)).isActive) // add first watchlist let setTx = await bm .connect(owner) .setWatchList([watchAddress1], [oneLINK], [twoLINK]) await setTx.wait() let watchList = await bm.getWatchList() assert.deepEqual(watchList, [watchAddress1]) const accountInfo = await bm.getAccountInfo(watchAddress1) assert.isTrue(accountInfo.isActive) expect(accountInfo.minBalance).to.equal(oneLINK) expect(accountInfo.topUpLevel).to.equal(twoLINK) // add more to watchlist setTx = await bm .connect(owner) .setWatchList( [watchAddress1, watchAddress2, watchAddress3], [oneLINK, twoLINK, threeLINK], [twoLINK, threeLINK, fiveLINK], ) await setTx.wait() watchList = await bm.getWatchList() assert.deepEqual(watchList, [watchAddress1, watchAddress2, watchAddress3]) let accountInfo1 = await bm.getAccountInfo(watchAddress1) let accountInfo2 = await bm.getAccountInfo(watchAddress2) let accountInfo3 = await bm.getAccountInfo(watchAddress3) expect(accountInfo1.isActive).to.be.true expect(accountInfo1.minBalance).to.equal(oneLINK) expect(accountInfo1.topUpLevel).to.equal(twoLINK) expect(accountInfo2.isActive).to.be.true expect(accountInfo2.minBalance).to.equal(twoLINK) expect(accountInfo2.topUpLevel).to.equal(threeLINK) expect(accountInfo3.isActive).to.be.true expect(accountInfo3.minBalance).to.equal(threeLINK) expect(accountInfo3.topUpLevel).to.equal(fiveLINK) // remove some from watchlist setTx = await bm .connect(owner) .setWatchList( [watchAddress3, watchAddress1], [threeLINK, oneLINK], [fiveLINK, twoLINK], ) await setTx.wait() watchList = await bm.getWatchList() assert.deepEqual(watchList, [watchAddress3, watchAddress1]) accountInfo1 = await bm.getAccountInfo(watchAddress1) accountInfo2 = await bm.getAccountInfo(watchAddress2) accountInfo3 = await bm.getAccountInfo(watchAddress3) expect(accountInfo1.isActive).to.be.true expect(accountInfo2.isActive).to.be.false expect(accountInfo3.isActive).to.be.true }) it('Should not allow duplicates in the watchlist', async () => { const errMsg = `DuplicateAddress` const setTx = bm .connect(owner) .setWatchList( [watchAddress1, watchAddress2, watchAddress1], [oneLINK, twoLINK, threeLINK], [twoLINK, threeLINK, fiveLINK], ) await expect(setTx) .to.be.revertedWithCustomError(bm, errMsg) .withArgs(watchAddress1) }) it('Should not allow a topUpLevel les than or equal to minBalance in the watchlist', async () => { const setTx = bm .connect(owner) .setWatchList( [watchAddress1, watchAddress2, watchAddress1], [oneLINK, twoLINK, threeLINK], [zeroLINK, twoLINK, threeLINK], ) await expect(setTx).to.be.revertedWithCustomError( bm, INVALID_WATCHLIST_ERR, ) }) it('Should not allow larger than maximum watchlist size', async () => { const watchlist: any[][] = [[], [], []] Array.from(Array(301).keys()).forEach(() => { watchlist[0].push(owner.address) watchlist[1].push(oneLINK) watchlist[2].push(twoLINK) }) const tx = bm .connect(owner) .setWatchList(watchlist[0], watchlist[1], watchlist[2]) await expect(tx).to.be.revertedWithCustomError(bm, INVALID_WATCHLIST_ERR) }) it('Should not allow strangers to set the watchlist', async () => { const setTxStranger = bm .connect(stranger) .setWatchList([watchAddress1], [oneLINK], [twoLINK]) await expect(setTxStranger).to.be.revertedWith(OWNABLE_ERR) }) it('Should revert if the list lengths differ', async () => { let tx = bm.connect(owner).setWatchList([watchAddress1], [], [twoLINK]) await expect(tx).to.be.revertedWithCustomError(bm, INVALID_WATCHLIST_ERR) tx = bm.connect(owner).setWatchList([watchAddress1], [oneLINK], []) await expect(tx).to.be.revertedWithCustomError(bm, INVALID_WATCHLIST_ERR) tx = bm.connect(owner).setWatchList([], [oneLINK], [twoLINK]) await expect(tx).to.be.revertedWithCustomError(bm, INVALID_WATCHLIST_ERR) }) it('Should revert if any of the addresses are empty', async () => { let tx = bm .connect(owner) .setWatchList( [watchAddress1, ethers.constants.AddressZero], [oneLINK, oneLINK], [twoLINK, twoLINK], ) await expect(tx).to.be.revertedWithCustomError(bm, INVALID_WATCHLIST_ERR) }) it('Should revert if any of the top up amounts are 0', async () => { const tx = bm .connect(owner) .setWatchList( [watchAddress1, watchAddress2], [oneLINK, oneLINK], [twoLINK, zeroLINK], ) await expect(tx).to.be.revertedWithCustomError(bm, INVALID_WATCHLIST_ERR) }) }) describe('getKeeperRegistryAddress() / setKeeperRegistryAddress()', () => { const newAddress = ethers.Wallet.createRandom().address it('Should initialize with the registry address provided to the constructor', async () => { const address = await bm.getKeeperRegistryAddress() assert.equal(address, keeperRegistry.address) }) it('Should allow the owner to set the registry address', async () => { const setTx = await bm.connect(owner).setKeeperRegistryAddress(newAddress) await setTx.wait() const address = await bm.getKeeperRegistryAddress() assert.equal(address, newAddress) }) it('Should not allow strangers to set the registry address', async () => { const setTx = bm.connect(stranger).setKeeperRegistryAddress(newAddress) await expect(setTx).to.be.revertedWith(OWNABLE_ERR) }) it('Should emit an event', async () => { const setTx = await bm.connect(owner).setKeeperRegistryAddress(newAddress) await expect(setTx) .to.emit(bm, 'KeeperRegistryAddressUpdated') .withArgs(keeperRegistry.address, newAddress) }) }) describe('getMinWaitPeriodSeconds / setMinWaitPeriodSeconds()', () => { const newWaitPeriod = BigNumber.from(1) it('Should initialize with the wait period provided to the constructor', async () => { const minWaitPeriod = await bm.getMinWaitPeriodSeconds() expect(minWaitPeriod).to.equal(0) }) it('Should allow owner to set the wait period', async () => { const setTx = await bm .connect(owner) .setMinWaitPeriodSeconds(newWaitPeriod) await setTx.wait() const minWaitPeriod = await bm.getMinWaitPeriodSeconds() expect(minWaitPeriod).to.equal(newWaitPeriod) }) it('Should not allow strangers to set the wait period', async () => { const setTx = bm.connect(stranger).setMinWaitPeriodSeconds(newWaitPeriod) await expect(setTx).to.be.revertedWith(OWNABLE_ERR) }) it('Should emit an event', async () => { const setTx = await bm .connect(owner) .setMinWaitPeriodSeconds(newWaitPeriod) await expect(setTx) .to.emit(bm, 'MinWaitPeriodUpdated') .withArgs(0, newWaitPeriod) }) }) describe('checkUpkeep() / getUnderfundedAddresses()', () => { beforeEach(async () => { const setTx = await bm.connect(owner).setWatchList( [ watchAddress1, // needs funds watchAddress5, // funded watchAddress2, // needs funds watchAddress6, // funded watchAddress3, // needs funds ], new Array(5).fill(oneLINK), new Array(5).fill(twoLINK), ) await setTx.wait() }) it('Should return list of address that are underfunded', async () => { const fundTx = await lt.connect(owner).transfer( bm.address, sixLINK, // needs 6 total ) await fundTx.wait() const [should, payload] = await bm.checkUpkeep('0x') assert.isTrue(should) let [addresses] = ethers.utils.defaultAbiCoder.decode( ['address[]'], payload, ) assert.deepEqual(addresses, [watchAddress1, watchAddress2, watchAddress3]) // checkUpkeep payload should match getUnderfundedAddresses() addresses = await bm.getUnderfundedAddresses() assert.deepEqual(addresses, [watchAddress1, watchAddress2, watchAddress3]) }) it('Should return some results even if contract cannot fund all eligible targets', async () => { const fundTx = await lt.connect(owner).transfer( bm.address, fiveLINK, // needs 6 total ) await fundTx.wait() const [should, payload] = await bm.checkUpkeep('0x') assert.isTrue(should) const [addresses] = ethers.utils.defaultAbiCoder.decode( ['address[]'], payload, ) assert.deepEqual(addresses, [watchAddress1, watchAddress2]) }) it('Should omit addresses that have been funded recently', async () => { const setWaitPdTx = await bm.setMinWaitPeriodSeconds(3600) // 1 hour const fundTx = await lt.connect(owner).transfer(bm.address, sixLINK) await Promise.all([setWaitPdTx.wait(), fundTx.wait()]) const block = await ethers.provider.getBlock('latest') const setTopUpTx = await bm.setLastTopUpXXXTestOnly( watchAddress2, block.timestamp - 100, ) await setTopUpTx.wait() const [should, payload] = await bm.checkUpkeep('0x') assert.isTrue(should) const [addresses] = ethers.utils.defaultAbiCoder.decode( ['address[]'], payload, ) assert.deepEqual(addresses, [watchAddress1, watchAddress3]) }) it('Should revert when paused', async () => { const tx = await bm.connect(owner).pause() await tx.wait() const ethCall = bm.checkUpkeep('0x') await expect(ethCall).to.be.revertedWith(PAUSED_ERR) }) }) describe('performUpkeep()', () => { let validPayload: string let invalidPayload: string beforeEach(async () => { validPayload = ethers.utils.defaultAbiCoder.encode( ['address[]'], [[watchAddress1, watchAddress2, watchAddress3]], ) invalidPayload = ethers.utils.defaultAbiCoder.encode( ['address[]'], [[watchAddress1, watchAddress2, watchAddress4, watchAddress5]], ) const setTx = await bm.connect(owner).setWatchList( [ watchAddress1, // needs funds watchAddress5, // funded watchAddress2, // needs funds watchAddress6, // funded watchAddress3, // needs funds // watchAddress4 - omitted ], new Array(5).fill(oneLINK), new Array(5).fill(twoLINK), ) await setTx.wait() }) it('Should revert when paused', async () => { const pauseTx = await bm.connect(owner).pause() await pauseTx.wait() const performTx = bm.connect(keeperRegistry).performUpkeep(validPayload) await expect(performTx).to.be.revertedWith(PAUSED_ERR) }) context('when partially funded', () => { it('Should fund as many addresses as possible', async () => { const fundTx = await lt.connect(owner).transfer( bm.address, fiveLINK, // only enough LINK to fund 2 addresses ) await fundTx.wait() await assertWatchlistBalances( zeroLINK, zeroLINK, zeroLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) const performTx = await bm .connect(keeperRegistry) .performUpkeep(validPayload) await assertWatchlistBalances( twoLINK, twoLINK, zeroLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) await expect(performTx) .to.emit(bm, 'TopUpSucceeded') .withArgs(watchAddress1) await expect(performTx) .to.emit(bm, 'TopUpSucceeded') .withArgs(watchAddress2) }) }) context('when fully funded', () => { beforeEach(async () => { const fundTx = await lt.connect(owner).transfer(bm.address, tenLINK) await fundTx.wait() }) it('Should fund the appropriate addresses', async () => { await assertWatchlistBalances( zeroLINK, zeroLINK, zeroLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) const performTx = await bm .connect(keeperRegistry) .performUpkeep(validPayload, { gasLimit: 2_500_000 }) await performTx.wait() await assertWatchlistBalances( twoLINK, twoLINK, twoLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) }) it('Should only fund active, underfunded addresses', async () => { await assertWatchlistBalances( zeroLINK, zeroLINK, zeroLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) const performTx = await bm .connect(keeperRegistry) .performUpkeep(invalidPayload, { gasLimit: 2_500_000 }) await performTx.wait() await assertWatchlistBalances( twoLINK, twoLINK, zeroLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) }) it('Should not fund addresses that have been funded recently', async () => { const setWaitPdTx = await bm.setMinWaitPeriodSeconds(3600) // 1 hour await setWaitPdTx.wait() const block = await ethers.provider.getBlock('latest') const setTopUpTx = await bm.setLastTopUpXXXTestOnly( watchAddress2, block.timestamp - 100, ) await setTopUpTx.wait() await assertWatchlistBalances( zeroLINK, zeroLINK, zeroLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) const performTx = await bm .connect(keeperRegistry) .performUpkeep(validPayload, { gasLimit: 2_500_000 }) await performTx.wait() await assertWatchlistBalances( twoLINK, zeroLINK, twoLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) }) it('Should only be callable by the keeper registry contract', async () => { let performTx = bm.connect(owner).performUpkeep(validPayload) await expect(performTx).to.be.revertedWithCustomError( bm, ONLY_KEEPER_ERR, ) performTx = bm.connect(stranger).performUpkeep(validPayload) await expect(performTx).to.be.revertedWithCustomError( bm, ONLY_KEEPER_ERR, ) }) it('Should protect against running out of gas', async () => { await assertWatchlistBalances( zeroLINK, zeroLINK, zeroLINK, zeroLINK, oneHundredLINK, oneHundredLINK, ) const performTx = await bm .connect(keeperRegistry) .performUpkeep(validPayload, { gasLimit: 130_000 }) // too little for all 3 transfers await performTx.wait() const balance1 = await lt.balanceOf(watchAddress1) const balance2 = await lt.balanceOf(watchAddress2) const balance3 = await lt.balanceOf(watchAddress3) const balances = [balance1, balance2, balance3].map((n) => n.toString()) expect(balances) .to.include(twoLINK.toString()) // expect at least 1 transfer .to.include(zeroLINK.toString()) // expect at least 1 out of funds }) it('Should provide enough gas to support receive and fallback functions', async () => { const addresses = [ receiveEmitter.address, receiveFallbackEmitter.address, ] const payload = ethers.utils.defaultAbiCoder.encode( ['address[]'], [addresses], ) const setTx = await bm .connect(owner) .setWatchList( addresses, new Array(2).fill(oneLINK), new Array(2).fill(twoLINK), ) await setTx.wait() const reBalanceBefore = await lt.balanceOf(receiveEmitter.address) const rfeBalanceBefore = await lt.balanceOf( receiveFallbackEmitter.address, ) const performTx = await bm .connect(keeperRegistry) .performUpkeep(payload, { gasLimit: 2_500_000 }) await h.assertLinkTokenBalance( lt, receiveEmitter.address, reBalanceBefore.add(twoLINK), ) await h.assertLinkTokenBalance( lt, receiveFallbackEmitter.address, rfeBalanceBefore.add(twoLINK), ) await expect(performTx) .to.emit(bm, 'TopUpSucceeded') .withArgs(receiveEmitter.address) await expect(performTx) .to.emit(bm, 'TopUpSucceeded') .withArgs(receiveFallbackEmitter.address) }) }) }) describe('topUp()', () => { context('when not paused', () => { it('Should be callable by anyone', async () => { const users = [owner, keeperRegistry, stranger] for (let idx = 0; idx < users.length; idx++) { const user = users[idx] await bm.connect(user).topUp([]) } }) }) context('when paused', () => { it('Should be callable by no one', async () => { await bm.connect(owner).pause() const users = [owner, keeperRegistry, stranger] for (let idx = 0; idx < users.length; idx++) { const user = users[idx] const tx = bm.connect(user).topUp([]) await expect(tx).to.be.revertedWith(PAUSED_ERR) } }) }) }) })