From 4a6f2fe749e19d92be013b9b8f59cefe829b07eb Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Fri, 10 Nov 2023 19:52:39 +0100 Subject: [PATCH] turn off 0.6 & 0.7 tests (#11132) * turn off 0.6 tests * reduce splits and mark other tests as sow * add modification protection * rm 0.7 tests --- .github/workflows/solidity.yml | 18 + contracts/ci.json | 22 +- contracts/test/v0.6/AggregatorFacade.test.ts | 167 - contracts/test/v0.6/BasicConsumer.test.ts | 243 -- contracts/test/v0.6/BlockhashStore.test.ts | 285 -- contracts/test/v0.6/Chainlink.test.ts | 186 - contracts/test/v0.6/ChainlinkClient.test.ts | 376 -- contracts/test/v0.6/CheckedMath.test.ts | 183 - .../v0.6/DeviationFlaggingValidator.test.ts | 296 -- contracts/test/v0.6/Flags.test.ts | 405 -- contracts/test/v0.6/FluxAggregator.test.ts | 3252 -------------- contracts/test/v0.6/Median.test.ts | 237 - contracts/test/v0.6/Owned.test.ts | 84 - contracts/test/v0.6/SignedSafeMath.test.ts | 187 - .../v0.6/SimpleReadAccessController.test.ts | 250 -- .../v0.6/SimpleWriteAccessController.test.ts | 214 - contracts/test/v0.6/VRFD20.test.ts | 303 -- contracts/test/v0.7/AggregatorProxy.test.ts | 743 ---- .../test/v0.7/AuthorizedForwarder.test.ts | 444 -- contracts/test/v0.7/Chainlink.test.ts | 186 - contracts/test/v0.7/ChainlinkClient.test.ts | 454 -- .../CompoundPriceFlaggingValidator.test.ts | 471 -- contracts/test/v0.7/ConfirmedOwner.test.ts | 136 - contracts/test/v0.7/KeeperRegistry1_1.test.ts | 1725 -------- contracts/test/v0.7/Operator.test.ts | 3819 ----------------- contracts/test/v0.7/OperatorFactory.test.ts | 293 -- .../v0.7/StalenessFlaggingValidator.test.ts | 632 --- .../v0.7/UpkeepRegistrationRequests.test.ts | 603 --- contracts/test/v0.7/VRFD20.test.ts | 303 -- contracts/test/v0.7/gasUsage.test.ts | 178 - 30 files changed, 27 insertions(+), 16668 deletions(-) delete mode 100644 contracts/test/v0.6/AggregatorFacade.test.ts delete mode 100644 contracts/test/v0.6/BasicConsumer.test.ts delete mode 100644 contracts/test/v0.6/BlockhashStore.test.ts delete mode 100644 contracts/test/v0.6/Chainlink.test.ts delete mode 100644 contracts/test/v0.6/ChainlinkClient.test.ts delete mode 100644 contracts/test/v0.6/CheckedMath.test.ts delete mode 100644 contracts/test/v0.6/DeviationFlaggingValidator.test.ts delete mode 100644 contracts/test/v0.6/Flags.test.ts delete mode 100644 contracts/test/v0.6/FluxAggregator.test.ts delete mode 100644 contracts/test/v0.6/Median.test.ts delete mode 100644 contracts/test/v0.6/Owned.test.ts delete mode 100644 contracts/test/v0.6/SignedSafeMath.test.ts delete mode 100644 contracts/test/v0.6/SimpleReadAccessController.test.ts delete mode 100644 contracts/test/v0.6/SimpleWriteAccessController.test.ts delete mode 100644 contracts/test/v0.6/VRFD20.test.ts delete mode 100644 contracts/test/v0.7/AggregatorProxy.test.ts delete mode 100644 contracts/test/v0.7/AuthorizedForwarder.test.ts delete mode 100644 contracts/test/v0.7/Chainlink.test.ts delete mode 100644 contracts/test/v0.7/ChainlinkClient.test.ts delete mode 100644 contracts/test/v0.7/CompoundPriceFlaggingValidator.test.ts delete mode 100644 contracts/test/v0.7/ConfirmedOwner.test.ts delete mode 100644 contracts/test/v0.7/KeeperRegistry1_1.test.ts delete mode 100644 contracts/test/v0.7/Operator.test.ts delete mode 100644 contracts/test/v0.7/OperatorFactory.test.ts delete mode 100644 contracts/test/v0.7/StalenessFlaggingValidator.test.ts delete mode 100644 contracts/test/v0.7/UpkeepRegistrationRequests.test.ts delete mode 100644 contracts/test/v0.7/VRFD20.test.ts delete mode 100644 contracts/test/v0.7/gasUsage.test.ts diff --git a/.github/workflows/solidity.yml b/.github/workflows/solidity.yml index 069d9de45ab..5699657fa5d 100644 --- a/.github/workflows/solidity.yml +++ b/.github/workflows/solidity.yml @@ -20,11 +20,29 @@ jobs: - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 id: changes with: + list-files: "csv" filters: | src: - 'contracts/**/*' - '.github/workflows/solidity.yml' - '.github/workflows/solidity-foundry.yml' + old_sol: + - 'contracts/src/v0.4/**/*' + - 'contracts/src/v0.5/**/*' + - 'contracts/src/v0.6/**/*' + - 'contracts/src/v0.7/**/*' + + + - name: Fail if read-only files have changed + if: ${{ steps.changes.outputs.old_sol == 'true' }} + run: | + echo "One or more read-only Solidity file(s) has changed." + for file in ${{ steps.changes.outputs.old_sol_files }}; do + echo "$file was changed" + done + exit 1 + + prepublish-test: needs: [changes] diff --git a/contracts/ci.json b/contracts/ci.json index dd1fbb0a88f..f1eff76513c 100644 --- a/contracts/ci.json +++ b/contracts/ci.json @@ -6,23 +6,19 @@ "dir": "cross-version", "numOfSplits": 1 }, - { - "dir": "v0.6", - "numOfSplits": 1 - }, - { - "dir": "v0.7", - "numOfSplits": 1 - }, { "dir": "v0.8", - "numOfSplits": 8, + "numOfSplits": 6, "slowTests": [ - "Keeper", - "Cron.test", - "CronUpkeep.test", + "Cron", + "CronUpkeep", + "VRFSubscriptionBalanceMonitor", "EthBalanceMonitor", - "CanaryUpkeep" + "KeeperRegistrar", + "KeeperRegistry1_2", + "KeeperRegistry1_3", + "KeeperRegistry2_0", + "KeeperRegistry2_1" ] } ] diff --git a/contracts/test/v0.6/AggregatorFacade.test.ts b/contracts/test/v0.6/AggregatorFacade.test.ts deleted file mode 100644 index f85c24ae6cd..00000000000 --- a/contracts/test/v0.6/AggregatorFacade.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { ethers } from 'hardhat' -import { numToBytes32, publicAbi } from '../test-helpers/helpers' -import { assert } from 'chai' -import { Contract, ContractFactory, Signer } from 'ethers' -import { getUsers } from '../test-helpers/setup' -import { convertFufillParams, decodeRunRequest } from '../test-helpers/oracle' -import { bigNumEquals, evmRevert } from '../test-helpers/matchers' - -let defaultAccount: Signer - -let linkTokenFactory: ContractFactory -let aggregatorFactory: ContractFactory -let oracleFactory: ContractFactory -let aggregatorFacadeFactory: ContractFactory - -before(async () => { - const users = await getUsers() - - defaultAccount = users.roles.defaultAccount - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - defaultAccount, - ) - aggregatorFactory = await ethers.getContractFactory( - 'src/v0.4/Aggregator.sol:Aggregator', - defaultAccount, - ) - oracleFactory = await ethers.getContractFactory( - 'src/v0.6/Oracle.sol:Oracle', - defaultAccount, - ) - aggregatorFacadeFactory = await ethers.getContractFactory( - 'src/v0.6/AggregatorFacade.sol:AggregatorFacade', - defaultAccount, - ) -}) - -describe('AggregatorFacade', () => { - const jobId1 = - '0x4c7b7ffb66b344fbaa64995af81e355a00000000000000000000000000000001' - const previousResponse = numToBytes32(54321) - const response = numToBytes32(67890) - const decimals = 18 - const description = 'LINK / USD: Historic Aggregator Facade' - - let link: Contract - let aggregator: Contract - let oc1: Contract - let facade: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(defaultAccount).deploy() - oc1 = await oracleFactory.connect(defaultAccount).deploy(link.address) - aggregator = await aggregatorFactory - .connect(defaultAccount) - .deploy(link.address, 0, 1, [oc1.address], [jobId1]) - facade = await aggregatorFacadeFactory - .connect(defaultAccount) - .deploy(aggregator.address, decimals, description) - - let requestTx = await aggregator.requestRateUpdate() - let receipt = await requestTx.wait() - let request = decodeRunRequest(receipt.logs?.[3]) - await oc1.fulfillOracleRequest( - ...convertFufillParams(request, previousResponse), - ) - requestTx = await aggregator.requestRateUpdate() - receipt = await requestTx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - await oc1.fulfillOracleRequest(...convertFufillParams(request, response)) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(facade, [ - 'aggregator', - 'decimals', - 'description', - 'getAnswer', - 'getRoundData', - 'getTimestamp', - 'latestAnswer', - 'latestRound', - 'latestRoundData', - 'latestTimestamp', - 'version', - ]) - }) - - describe('#constructor', () => { - it('uses the decimals set in the constructor', async () => { - bigNumEquals(decimals, await facade.decimals()) - }) - - it('uses the description set in the constructor', async () => { - assert.equal(description, await facade.description()) - }) - - it('sets the version to 2', async () => { - bigNumEquals(2, await facade.version()) - }) - }) - - describe('#getAnswer/latestAnswer', () => { - it('pulls the rate from the aggregator', async () => { - bigNumEquals(response, await facade.latestAnswer()) - const latestRound = await facade.latestRound() - bigNumEquals(response, await facade.getAnswer(latestRound)) - }) - }) - - describe('#getTimestamp/latestTimestamp', () => { - it('pulls the timestamp from the aggregator', async () => { - const height = await aggregator.latestTimestamp() - assert.notEqual('0', height.toString()) - bigNumEquals(height, await facade.latestTimestamp()) - const latestRound = await facade.latestRound() - bigNumEquals( - await aggregator.latestTimestamp(), - await facade.getTimestamp(latestRound), - ) - }) - }) - - describe('#getRoundData', () => { - it('assembles the requested round data', async () => { - const previousId = (await facade.latestRound()).sub(1) - const round = await facade.getRoundData(previousId) - bigNumEquals(previousId, round.roundId) - bigNumEquals(previousResponse, round.answer) - bigNumEquals(await facade.getTimestamp(previousId), round.startedAt) - bigNumEquals(await facade.getTimestamp(previousId), round.updatedAt) - bigNumEquals(previousId, round.answeredInRound) - }) - - it('returns zero data for non-existing rounds', async () => { - const roundId = 13371337 - await evmRevert(facade.getRoundData(roundId), 'No data present') - }) - }) - - describe('#latestRoundData', () => { - it('assembles the requested round data', async () => { - const latestId = await facade.latestRound() - const round = await facade.latestRoundData() - bigNumEquals(latestId, round.roundId) - bigNumEquals(response, round.answer) - bigNumEquals(await facade.getTimestamp(latestId), round.startedAt) - bigNumEquals(await facade.getTimestamp(latestId), round.updatedAt) - bigNumEquals(latestId, round.answeredInRound) - }) - - describe('when there is no latest round', () => { - beforeEach(async () => { - aggregator = await aggregatorFactory - .connect(defaultAccount) - .deploy(link.address, 0, 1, [oc1.address], [jobId1]) - facade = await aggregatorFacadeFactory - .connect(defaultAccount) - .deploy(aggregator.address, decimals, description) - }) - - it('assembles the requested round data', async () => { - await evmRevert(facade.latestRoundData(), 'No data present') - }) - }) - }) -}) diff --git a/contracts/test/v0.6/BasicConsumer.test.ts b/contracts/test/v0.6/BasicConsumer.test.ts deleted file mode 100644 index ce0b7c643e2..00000000000 --- a/contracts/test/v0.6/BasicConsumer.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { ethers } from 'hardhat' -import { toWei, increaseTime5Minutes, toHex } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { BigNumber, constants, Contract, ContractFactory } from 'ethers' -import { Roles, getUsers } from '../test-helpers/setup' -import { bigNumEquals, evmRevert } from '../test-helpers/matchers' -import { - convertFufillParams, - decodeRunRequest, - encodeOracleRequest, - RunRequest, -} from '../test-helpers/oracle' -import cbor from 'cbor' -import { makeDebug } from '../test-helpers/debug' - -const d = makeDebug('BasicConsumer') -let basicConsumerFactory: ContractFactory -let oracleFactory: ContractFactory -let linkTokenFactory: ContractFactory - -let roles: Roles - -before(async () => { - roles = (await getUsers()).roles - basicConsumerFactory = await ethers.getContractFactory( - 'src/v0.6/tests/BasicConsumer.sol:BasicConsumer', - roles.defaultAccount, - ) - oracleFactory = await ethers.getContractFactory( - 'src/v0.6/Oracle.sol:Oracle', - roles.oracleNode, - ) - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) -}) - -describe('BasicConsumer', () => { - const specId = '0x4c7b7ffb66b344fbaa64995af81e355a'.padEnd(66, '0') - const currency = 'USD' - const payment = toWei('1') - let link: Contract - let oc: Contract - let cc: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - oc = await oracleFactory.connect(roles.oracleNode).deploy(link.address) - cc = await basicConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, oc.address, specId) - }) - - it('has a predictable gas price [ @skip-coverage ]', async () => { - const rec = await ethers.provider.getTransactionReceipt( - cc.deployTransaction.hash ?? '', - ) - assert.isBelow(rec.gasUsed?.toNumber() ?? -1, 1750000) - }) - - describe('#requestEthereumPrice', () => { - describe('without LINK', () => { - it('reverts', async () => - await expect(cc.requestEthereumPrice(currency, payment)).to.be.reverted) - }) - - describe('with LINK', () => { - beforeEach(async () => { - await link.transfer(cc.address, toWei('1')) - }) - - it('triggers a log event in the Oracle contract', async () => { - const tx = await cc.requestEthereumPrice(currency, payment) - const receipt = await tx.wait() - - const log = receipt?.logs?.[3] - assert.equal(log?.address.toLowerCase(), oc.address.toLowerCase()) - - const request = decodeRunRequest(log) - const expected = { - path: ['USD'], - get: 'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,JPY', - } - - assert.equal(toHex(specId), request.specId) - bigNumEquals(toWei('1'), request.payment) - assert.equal(cc.address.toLowerCase(), request.requester.toLowerCase()) - assert.equal(1, request.dataVersion) - assert.deepEqual(expected, cbor.decodeFirstSync(request.data)) - }) - - it('has a reasonable gas cost [ @skip-coverage ]', async () => { - const tx = await cc.requestEthereumPrice(currency, payment) - const receipt = await tx.wait() - - assert.isBelow(receipt?.gasUsed?.toNumber() ?? -1, 140000) - }) - }) - }) - - describe('#fulfillOracleRequest', () => { - const response = ethers.utils.formatBytes32String('1,000,000.00') - let request: RunRequest - - beforeEach(async () => { - await link.transfer(cc.address, toWei('1')) - const tx = await cc.requestEthereumPrice(currency, payment) - const receipt = await tx.wait() - - request = decodeRunRequest(receipt?.logs?.[3]) - }) - - it('records the data given to it by the oracle', async () => { - await oc - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - const currentPrice = await cc.currentPrice() - assert.equal(currentPrice, response) - }) - - it('logs the data given to it by the oracle', async () => { - const tx = await oc - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - const receipt = await tx.wait() - - assert.equal(2, receipt?.logs?.length) - const log = receipt?.logs?.[1] - - assert.equal(log?.topics[2], response) - }) - - describe('when the consumer does not recognize the request ID', () => { - let otherRequest: RunRequest - - beforeEach(async () => { - // Create a request directly via the oracle, rather than through the - // chainlink client (consumer). The client should not respond to - // fulfillment of this request, even though the oracle will faithfully - // forward the fulfillment to it. - const args = encodeOracleRequest( - toHex(specId), - cc.address, - basicConsumerFactory.interface.getSighash('fulfill'), - 43, - constants.HashZero, - ) - const tx = await link.transferAndCall(oc.address, 0, args) - const receipt = await tx.wait() - - otherRequest = decodeRunRequest(receipt?.logs?.[2]) - }) - - it('does not accept the data provided', async () => { - d('otherRequest %s', otherRequest) - await oc - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(otherRequest, response)) - - const received = await cc.currentPrice() - - assert.equal(ethers.utils.parseBytes32String(received), '') - }) - }) - - describe('when called by anyone other than the oracle contract', () => { - it('does not accept the data provided', async () => { - await evmRevert( - cc.connect(roles.oracleNode).fulfill(request.requestId, response), - ) - - const received = await cc.currentPrice() - assert.equal(ethers.utils.parseBytes32String(received), '') - }) - }) - }) - - describe('#cancelRequest', () => { - const depositAmount = toWei('1') - let request: RunRequest - - beforeEach(async () => { - await link.transfer(cc.address, depositAmount) - const tx = await cc.requestEthereumPrice(currency, payment) - const receipt = await tx.wait() - - request = decodeRunRequest(receipt.logs?.[3]) - }) - - describe('before 5 minutes', () => { - it('cant cancel the request', () => - evmRevert( - cc - .connect(roles.consumer) - .cancelRequest( - oc.address, - request.requestId, - request.payment, - request.callbackFunc, - request.expiration, - ), - )) - }) - - describe('after 5 minutes', () => { - it('can cancel the request', async () => { - await increaseTime5Minutes(ethers.provider) - - await cc - .connect(roles.consumer) - .cancelRequest( - oc.address, - request.requestId, - request.payment, - request.callbackFunc, - request.expiration, - ) - }) - }) - }) - - describe('#withdrawLink', () => { - const depositAmount = toWei('1') - - beforeEach(async () => { - await link.transfer(cc.address, depositAmount) - const balance = await link.balanceOf(cc.address) - bigNumEquals(balance, depositAmount) - }) - - it('transfers LINK out of the contract', async () => { - await cc.connect(roles.consumer).withdrawLink() - const ccBalance = await link.balanceOf(cc.address) - const consumerBalance = BigNumber.from( - await link.balanceOf(await roles.consumer.getAddress()), - ) - bigNumEquals(ccBalance, 0) - bigNumEquals(consumerBalance, depositAmount) - }) - }) -}) diff --git a/contracts/test/v0.6/BlockhashStore.test.ts b/contracts/test/v0.6/BlockhashStore.test.ts deleted file mode 100644 index 453b2eca3b1..00000000000 --- a/contracts/test/v0.6/BlockhashStore.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { ethers } from 'hardhat' -import { assert, expect } from 'chai' -import { Contract, ContractFactory } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' - -let personas: Personas -let blockhashStoreTestHelperFactory: ContractFactory - -type TestBlocks = { - num: number - rlpHeader: Uint8Array - hash: string -} - -const mainnetBlocks: TestBlocks[] = [ - { - num: 10000467, - rlpHeader: ethers.utils.arrayify( - '0xf90215a058ee3c05e880cb25a3db92b9f1479c5453690ca97f9bcbb18d21965d3213578ea01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ea674fdde714fd979de3edf0f56aa9716b898ec8a0a448355652812a7d518b5c979a15bba02cfe4576d8eb61e8b5731ecc37f2bec6a0049f25ed97f9ed9a9c8521ab39cd2c48438d1d18c84dcab5bf494c19595bd462a0b1169f28bdbe5dd61ebc20b7a459be9d7fa898f5a3ba5fed6d502d94b9a8101bb901001000008180000210000080010001080310e004800c3040000060484000010804088050044302a500240041040010012120840002400005092000808000640012081000880010008040200208000004050800400002244044006041040040010890040504020040008004222502000800220000021800006400802036500000000400014640d00020002110000001440000001509543802080004210004100de04744a2810000000032250080810000502210c04289480800000423080800004000a020220030203000020001000000042c00420090000008003308459020e010a01000200190900040e81000040040000020000a8044001000202010000600c087086c49cadb1b57839898538398909483984b9e845eb02fbf94505059452d65746865726d696e652d6575312d34a06d0287c21536fac432714bd3f3712ff1a7e409faf1b10edac9b9547da1d4f7b188930531280477460c', - ), - hash: '0x4a65bcdf3466a16740b74849cc10fc57d4acb24cce148665482812699a400464', - }, - { - num: 10000468, - rlpHeader: ethers.utils.arrayify( - '0xf9020da04a65bcdf3466a16740b74849cc10fc57d4acb24cce148665482812699a400464a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479404668ec2f57cc15c381b461b9fedab5d451c8f7fa0bcd4ddbb7125a5c06df96d862921dc0bba8664b3f759a233fe565a615c0ab3eaa0087ab379852c83e4b660de1668fc93333201ad0d233167ea6cef8bacaf5cba2aa0d81855037b2a6b56eba0c2ed129fb4102fb831b0baa187a0f6e0c155400f7855b9010080040040200000000010102081000000500040010408040800010110000000008000005808020000902021818000210000000000081100401000400014400001041008000020448800180128800008000200000420e01200000000000000011000001000020000208000b42200a0008000510200080200008c002018108010014030200000080000000002000010008000011008004003081000400080100803040080040300000002044080480000000000008080101000000050000000000840000002200040000a0080000442008006005502800000040008000890201002022402208002900020900000000080000100100201080000000003400000004887086d57541477ba839898548398968083989147845eb02fc28c73706964657230380b03ac53a076c676a0ab090b373b6242851a4beab7b8cdc9d3ebe211747a255b78c0278c42880ea13d40042dd1e6', - ), - hash: '0x00fd2589a272b85ffaf63223641571bf95891c936b7514ee4e87a593e52de7c9', - }, - { - num: 10000469, - rlpHeader: ethers.utils.arrayify( - '0xf90211a000fd2589a272b85ffaf63223641571bf95891c936b7514ee4e87a593e52de7c9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347945a0b54d5dc17e0aadc383d2db43b0a0d3e029c4ca01b28d3b4e4d3442a9e6caed9f80b6a639bce6f3a283d4e004e6bb44e483ceeeba067c00d9067bc023b8fab8e3afd1bc0f2470f08003bdf9f167fbfeede2422ac4ea09d8b344d9ab1b7288f8c5e644c53b1a5288da6d6ee0e388ec76f291def48da15b90100c462095870a26a0804132e208110329710d459054558159c103208d44820002016108136199200061063699d8400254a22828c11b5512e3303c98ec7747cc02d00161880c2f2c580e806bccc04805190265f096601342058020a8324c277735d8202220412f03303201252a3000038883a4bb0010e6b004408306232150a84d110100d0c4b9d228022812602c05c801d20500d4ed10010ce2400428a96950a98050c00e603292a806c4983b25814880000440a23821191121996410c5110c949616c2066a4a0488087d4c226c14208042c00d609b5cc44051400219d93626818728612a9b18690e03c902014a900e0018828011494b80d4708799b0d8a83cace87086e64fefefb48839898558398968083986664845eb02fc7906574682d70726f2d687a662d74303032a09f1918a362b55ebd072cc9548fb74f89301d41c2a1feb13c08a1c2c3cb0606d88810dfa530069367fb', - ), - hash: '0x325fde74e261fc483a16506bbc711b645b043ad24c7db0136845a1be262cf0c9', - }, - { - num: 10000470, - rlpHeader: ethers.utils.arrayify( - '0xf90215a0325fde74e261fc483a16506bbc711b645b043ad24c7db0136845a1be262cf0c9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ea674fdde714fd979de3edf0f56aa9716b898ec8a020647cfa35563093442a12d80bf2bacb83da1de8340366677f3822591a334ccea066ad7285f6c5b6407f62c6b65a83aeaaa71ad9a97c2bb15139140f2dbb60f7e0a0c0e633851d0b5ce661ecc054517425e82425fcc6170db9693e5b5a6dd5ef6d6bb90100c0c000c1520708182080c8e461891c2402800a80d44a00034259414012012a5006a1416331181504902044960808f1129018800311621e920886804693749b10542400142e984580ccba634881c4156962200ecfb004000005468db44842781c59923110262660802315006106388b028412c42c000820c508e66b7851fa68002008144cd7860cd884280802915163399c168d5a11b0649486084110149469a1e61c31134204b903206566885180bc0426c0c6c0a4d408e182242f08180d204c624a040248425041ac028010d088820402ba4bd38c2d1215829300543465603822110500811290490148049300040e000c280086a09e8100089818ce480a887e87086c4965bf3c8a839898568398705c839847d2845eb02fe994505059452d65746865726d696e652d6575312d35a09d8ae288d0eede524f3ef5e6cfcc5ba07f380bc695bb71578a7b91cfa517071b8859d0976006378e52', - ), - hash: '0x5cf096dfd1fc2d2947a96fdec5377ab7beaa0eb00c80728a3b96f0864cec506a', - }, -] - -const maticBlocks: TestBlocks[] = [ - { - num: 10000467, - rlpHeader: ethers.utils.arrayify( - '0xf9025da0212093b89337e6741aca0c6c1cbfc64b56155bdcc3623fa9bcbfa0498fa135aba01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0ac0ec242516093308f7a2cc6965f38835eb3db69cba93401daef672666a3aefea0d06985e9ae671d22fb4b3d73ef4e074a448f66b4769ad8d59c3b8aef1ede15e2a00076d4897a88e08c25ca12c558f622d03d532d8b674e8c6b9847000b98dbe480b90100040000000200000000000000000000000000000000000000100000000000400000000000000000000000002800000000100080000000000000000001000000400000000000000000000000080002008000000000000000000021000000000000000000000200000000001000000008000000000008000001800800100000000000010000000010100000800000000000001000000200100000000000000000002000000004000000000000000080010000000000000000200000000000000040000000420000000000010000000000000004040004000000001000001000200100100080000000000400000000100000100000000000000000000000021000000e839898538401312d008302e54b84600df884b861d78301091883626f7288676f312e31352e35856c696e7578000000000000000003eb49c29f5facd767206f64b8a5c9b325bced5c9156f489c6281c68eddc9e5f2ef1177c02a99d8ab6216dcf2879eefddfc27c75ffa9ef6a2185ce9983d1434901a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0x6c3b869ca26fece236545f7914d8249651d729852dc1445f53a94d5a59cdc9da', - }, - { - num: 10000468, - rlpHeader: ethers.utils.arrayify( - '0xf9025da06c3b869ca26fece236545f7914d8249651d729852dc1445f53a94d5a59cdc9daa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0fa236c78bbe5939cc62985e32582c2158468a5b2b4dd02d514edb0bea95f0fd3a0e05ccfb09764e5cd6811ef2c2616d4a57f187be84235e2569c9b8d70489f1a44a0aea27aed2ad1d553e30501e6fe47fee0842c3b7ce5867e579b29975f02ec4282b90100008000100000000000000400080800000009000000010020000000000800000000000000080000000000000000000000000080000080000820400000000000000000200000000000000000080000008000200000200000000003009000020000000200000010000000001000000000000000000000000000800040100000000000000000000010000000100100000000000000000102004000000040000000002000000008000000000000000000000000000000200000000000000000000041000000020000080001010000000000000008000000110000001001800020000000100000000001400000040000000000000010010000000001000000001000000e839898548401312d00830494ed84600df886b861d78301091883626f7288676f312e31352e35856c696e75780000000000000000aa8ed86143b48b6aa7170d2083c3a7be31cbdfdc40f39badb8747f4c2198279a71c0d3eb5d25f3b7da5a48b887f61e22fe0baa692aa03807ad12f6fe25af087e00a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0x258aa48bde013579fbfef2e222bcc222b1f57bf898a71c623f9024229c9f6111', - }, - { - num: 10000469, - rlpHeader: ethers.utils.arrayify( - '0xf9025aa0258aa48bde013579fbfef2e222bcc222b1f57bf898a71c623f9024229c9f6111a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0fa236c78bbe5939cc62985e32582c2158468a5b2b4dd02d514edb0bea95f0fd3a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e839898558401312d008084600df888b861d78301091883626f7288676f312e31352e35856c696e75780000000000000000bd8668cc5d89583a7cc26fb96650e61f045ffe5248ae80c667ba7648df41e3d552060998ac151f2d15bd1b98f0a2a50c4281729a4c0aae4758a3bad280207c2901a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0x611779767f1deb5a17723ec71d1b397b18a0fc9a40d282810a33bd6a0a5f46f9', - }, - { - num: 10000470, - rlpHeader: ethers.utils.arrayify( - '0xf9025aa0611779767f1deb5a17723ec71d1b397b18a0fc9a40d282810a33bd6a0a5f46f9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0fa236c78bbe5939cc62985e32582c2158468a5b2b4dd02d514edb0bea95f0fd3a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e839898568401312d008084600df88ab861d78301091883626f7288676f312e31352e35856c696e75780000000000000000b617675c3b01e98319508130e1a583d57ce6b3a8a97fa2fbdaa33673cc6c609d6f7c361c833838f54b724d3a83cdd73e2398bb147970cd0b057865386cb08e1300a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0x2edf2f5c5faa5046b2304f76c92096a25e7c4343a7b75c36b29e8e9755d93397', - }, -] - -// The following headers from Binance Smart Chain were retrieved using `go run -// binance.go`, where binance.go contains -// -// package main -// -// import ( -// "context" -// "fmt" -// "log" -// "math/big" -// "math/rand" -// "strings" -// -// "github.com/ethereum/go-ethereum/ethclient" -// "github.com/ethereum/go-ethereum/rlp" -// ) -// -// var tsBlockTemplate = ` -// { -// num: %d, -// rlpHeader: ethers.utils.arrayify( -// '0x%x', -// ), -// hash: '0x%x', -// }, -// ` -// -// func main() { -// client, err := ethclient.Dial("https://bsc-dataseed.binance.org/") -// if err != nil { -// log.Fatal(err) -// } -// -// header, err := client.HeaderByNumber(context.Background(), nil) -// if err != nil { -// log.Fatal(err) -// } -// topBlockNum := header.Number.Int64() -// numBlocks := int64(4) -// if topBlockNum < numBlocks { -// log.Fatalf("need at least %d consecutive blocks", numBlocks) -// } -// targetBlock := int64(rand.Intn(int(topBlockNum - numBlocks))) -// simulatedHeadBlock := targetBlock + numBlocks - 1 -// for blockNum := targetBlock; blockNum <= simulatedHeadBlock; blockNum++ { -// header, err := client.HeaderByNumber(context.Background(), big.NewInt(blockNum)) -// if err != nil { -// log.Fatal(err) -// } -// s, err := rlp.EncodeToBytes(header) -// if err != nil { -// log.Fatalf("could not encode header: got error %s from %v", err, header) -// } -// // fmt.Printf("header for block number %d: 0x%x\n", blockNum, s) -// fmt.Printf(strings.TrimLeft(tsBlockTemplate, "\n"), blockNum, s, header.Hash()) -// } -// } -const binanceBlocks: TestBlocks[] = [ - { - num: 1875651, - rlpHeader: ethers.utils.arrayify( - '0xf9025da029c26248bebbe0d0acb209d13ac9337c4b5c313696c031dd63b3cd16cbdc0c21a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794b8f7166496996a7da21cf1f1b04d9b3e26a3d077a03f962867b5e86191c3280bd52c4249587e08ddfa9851cea981fb7a5721c9157aa05924ae05d17347687ba81d093aee159ccc65cefc8314b0515ef921e553df05a2a089af99a7afa586e7d67062d051df4255304bb730f6d62fdd3bdb207f1513b23bb901000100000000000000000800000000000000000000000200000000000000800000000000000200100000000000000800000000000000000000000000000000000000000000000000800000140800000008201000001000000202000000001200000000002002020000000000000000080000000000000002000000001000000000000002000000008010000000000000000002040080008400280000c00000081000400000004000000010000000020000000000000000000000000000000000000001000210200000000000000000000800000000000000000000000000002010000004000000000001000000000000000000000800020000000000000000000002831c9ec38401c9c380830789c2845f9faab1b861d883010002846765746888676f312e31332e34856c696e7578000000000000003311ee6830f31dc9116d8a59178b539d91eb6811c1d533c4a59bf77262689c552218bb1eae9cb9d6bf6e1066bea78052c8767313ace71c919d02e70760bd255401a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0xe0a935b1e37420ac1d855215bdad4730a5ffe315eda287c6c18aa86c426ede74', - }, - { - num: 1875652, - rlpHeader: ethers.utils.arrayify( - '0xf9025da0e0a935b1e37420ac1d855215bdad4730a5ffe315eda287c6c18aa86c426ede74a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794c2be4ec20253b8642161bc3f444f53679c1f3d47a0dbf2d40cf5533b65ac9b5be35cac75f7b106244ba14ee476a832c28d46a53452a04f83b8a51d3e17b6a02a0caa79acc7597996c5b8c68dba12c33095ae086089eea02fa2642645b2de17227a6c18c3fa491f54b3bdfe8ac8e04924a33a005a0e9e61b901000100000100000000000008000000000000000000040000000000000000800000000000000000000000000000000800000800000000000400000000000020000040100080000000000000000800000000209000001000000200000000801000400800002002030000000000000100080000002000000002004000011000000002000100040000000000100000000000000000040100009000300000000000000002004000004000000000000000020000002000000010000000200000800000000001000280000000000000008000000000000000800000000000020000002000041000000000000001200020001000080000002a40020040000000000000000002831c9ec48401c9c38083044b40845f9faab4b861d883010002846765746888676f312e31332e34856c696e757800000000000000cfc02687b2394922055792a8e67dad566f6690de06b229d752433b2067207b5f43b9f3c63f91cea5a79bbfc51d9132b933a706ab504038a92f37d57af2bb6c2e01a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0x629e5abcae42940e00d7b38aa7b2ecccfbab582cb7a0b2c3658c2dad8e66549d', - }, - { - num: 1875653, - rlpHeader: ethers.utils.arrayify( - '0xf9025da0629e5abcae42940e00d7b38aa7b2ecccfbab582cb7a0b2c3658c2dad8e66549da01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ce2fd7544e0b2cc94692d4a704debef7bcb61328a0718e7db53041585a814d658c32c88fd550c2c6d200826020008925e0a0f7967fa000fbf842e492a47cc9786885783259a08aed71055e78216006408932515fd960a0c7ffeb2189b8fcde43733cf1958cdb1c38c44052cfbb41125382240c232a98f8b901000000000000000000000000000000000000000002000000000004000000000000000000010000000000000000000000000000000000000200000000004020200000010000000800000000208800000000201000000000000000080000000000000000002002220000000000000000080000000000000000000000001000000000100000000000080010000000000000000000040000000000000000000000000002000000000008000000004000000000000000000000200000000000000000000000000202000000000000000000000000000000000008000000000000002080001000000000000001000000000000000000080100000000000000000000000002831c9ec58401c9c38083025019845f9faab7b861d883010002846765746888676f312e31332e34856c696e7578000000000000008c3c7a5c83e930fbd9d14f83c9b3931f032f0f678919c35b8b32ca6dae9948950bfa326fae134fa234fa7b84c06bdc3f7c6d6414c2a266df1339e563be8bd9cc00a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0xae8574651adabfd0ca55e2cee0e2e639ced73ec1cc0a35debeeceee6943442a9', - }, - { - num: 1875654, - rlpHeader: ethers.utils.arrayify( - '0xf9025da0ae8574651adabfd0ca55e2cee0e2e639ced73ec1cc0a35debeeceee6943442a9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794d6caa02bbebaebb5d7e581e4b66559e635f805ffa02df6a1173c63ec0a8acc46c818670f030aece1154b9f3bbc70f46a8427dd8dd6a0fa8835c499682d8c90759ff9ea1c291048755b967e48880a0fc21d19ec034a59a0b4e22607cb105c04156044b3f98c2cecae1553b45aa9b6044c37573791a27576b901000200000008000000000001000000000000000000000020000000000000020000000000000000000000000000000000000000000000000040000000000220000000000000000400000000001802000000201000000000000000000000000000000000002002020000000000000000080000000000000000000000001000000000000000000000100000000000000000000000040000000000000000010200200002000400000000400000000200000000000000080000000000000000000008000000000200000000000000000000000000000000000000000000000000002000001000000000000001000000000000000000000000000000000008080000000002831c9ec68401c9c3808301e575845f9faabab861d883010002846765746888676f312e31332e34856c696e757800000000000000399e73b0e963ec029e815623a414aa852508a28dd9799a1bf4e2380c8db687a46cc5b6cc20352ae21e35cfd28124a32fcd49ac8fac5b03901b3e03963e4fff5801a00000000000000000000000000000000000000000000000000000000000000000880000000000000000', - ), - hash: '0x189990455c59a5dea78071df9a2008ede292ff0a062fc5c4c6ca35fbe476f834', - }, -] - -before(async () => { - personas = (await getUsers()).personas - blockhashStoreTestHelperFactory = await ethers.getContractFactory( - 'src/v0.6/tests/BlockhashStoreTestHelper.sol:BlockhashStoreTestHelper', - personas.Default, - ) -}) - -runBlockhashStoreTests(mainnetBlocks, 'Ethereum') -runBlockhashStoreTests(maticBlocks, 'Matic') -runBlockhashStoreTests(binanceBlocks, 'Binance Smart Chain') - -async function runBlockhashStoreTests( - blocks: TestBlocks[], - description: string, -) { - describe(`BlockhashStore (${description})`, () => { - let blockhashStoreTestHelper: Contract - - beforeEach(async () => { - blockhashStoreTestHelper = await blockhashStoreTestHelperFactory - .connect(personas.Default) - .deploy() - - const [lastBlock] = blocks.slice(-1) - await blockhashStoreTestHelper - .connect(personas.Default) - .godmodeSetHash(lastBlock.num, lastBlock.hash) - assert.strictEqual( - await blockhashStoreTestHelper.getBlockhash(lastBlock.num), - lastBlock.hash, - ) - }) - - it('getBlockhash reverts for unknown blockhashes', async () => { - await expect( - blockhashStoreTestHelper.getBlockhash(99999999), - ).to.be.revertedWith('blockhash not found in store') - }) - - it('storeVerifyHeader records valid blockhashes', async () => { - for (let i = blocks.length - 2; i >= 0; i--) { - assert.strictEqual( - ethers.utils.keccak256(blocks[i + 1].rlpHeader), - await blockhashStoreTestHelper.getBlockhash(blocks[i + 1].num), - ) - await blockhashStoreTestHelper - .connect(personas.Default) - .storeVerifyHeader(blocks[i].num, blocks[i + 1].rlpHeader) - assert.strictEqual( - await blockhashStoreTestHelper.getBlockhash(blocks[i].num), - blocks[i].hash, - ) - } - }) - - it('storeVerifyHeader rejects unknown headers', async () => { - const unknownBlock = blocks[0] - await expect( - blockhashStoreTestHelper - .connect(personas.Default) - .storeVerifyHeader(unknownBlock.num - 1, unknownBlock.rlpHeader), - ).to.be.revertedWith('header has unknown blockhash') - }) - - it('storeVerifyHeader rejects corrupted headers', async () => { - const [lastBlock] = blocks.slice(-1) - const modifiedHeader = new Uint8Array(lastBlock.rlpHeader) - modifiedHeader[137] += 1 - await expect( - blockhashStoreTestHelper - .connect(personas.Default) - .storeVerifyHeader(lastBlock.num - 1, modifiedHeader), - ).to.be.revertedWith('header has unknown blockhash') - }) - - it('store accepts recent block numbers', async () => { - await ethers.provider.send('evm_mine', []) - - const n = (await ethers.provider.getBlockNumber()) - 1 - await blockhashStoreTestHelper.connect(personas.Default).store(n) - - assert.equal( - await blockhashStoreTestHelper.getBlockhash(n), - (await ethers.provider.getBlock(n)).hash, - ) - }) - - it('store rejects future block numbers', async () => { - await expect( - blockhashStoreTestHelper.connect(personas.Default).store(99999999999), - ).to.be.revertedWith('blockhash(n) failed') - }) - - it('store rejects old block numbers', async () => { - for (let i = 0; i < 300; i++) { - await ethers.provider.send('evm_mine', []) - } - - await expect( - blockhashStoreTestHelper - .connect(personas.Default) - .store((await ethers.provider.getBlockNumber()) - 256), - ).to.be.revertedWith('blockhash(n) failed') - }) - - it('storeEarliest works', async () => { - for (let i = 0; i < 300; i++) { - await ethers.provider.send('evm_mine', []) - } - - await blockhashStoreTestHelper.connect(personas.Default).storeEarliest() - - const n = (await ethers.provider.getBlockNumber()) - 256 - assert.equal( - await blockhashStoreTestHelper.getBlockhash(n), - (await ethers.provider.getBlock(n)).hash, - ) - }) - }) -} diff --git a/contracts/test/v0.6/Chainlink.test.ts b/contracts/test/v0.6/Chainlink.test.ts deleted file mode 100644 index f3587dc30a7..00000000000 --- a/contracts/test/v0.6/Chainlink.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi, decodeDietCBOR, hexToBuf } from '../test-helpers/helpers' -import { assert } from 'chai' -import { Contract, ContractFactory, providers, Signer } from 'ethers' -import { Roles, getUsers } from '../test-helpers/setup' -import { makeDebug } from '../test-helpers/debug' - -const debug = makeDebug('ChainlinkTestHelper') -let concreteChainlinkFactory: ContractFactory - -let roles: Roles - -before(async () => { - roles = (await getUsers()).roles - concreteChainlinkFactory = await ethers.getContractFactory( - 'src/v0.6/tests/ChainlinkTestHelper.sol:ChainlinkTestHelper', - roles.defaultAccount, - ) -}) - -describe('ChainlinkTestHelper', () => { - let ccl: Contract - let defaultAccount: Signer - - beforeEach(async () => { - defaultAccount = roles.defaultAccount - ccl = await concreteChainlinkFactory.connect(defaultAccount).deploy() - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(ccl, [ - 'add', - 'addBytes', - 'addInt', - 'addStringArray', - 'addUint', - 'closeEvent', - 'setBuffer', - ]) - }) - - async function parseCCLEvent(tx: providers.TransactionResponse) { - const receipt = await tx.wait() - const data = receipt.logs?.[0].data - const d = debug.extend('parseCCLEvent') - d('data %s', data) - return ethers.utils.defaultAbiCoder.decode(['bytes'], data ?? '') - } - - describe('#close', () => { - it('handles empty payloads', async () => { - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, {}) - }) - }) - - describe('#setBuffer', () => { - it('emits the buffer', async () => { - await ccl.setBuffer('0xA161616162') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { a: 'b' }) - }) - }) - - describe('#add', () => { - it('stores and logs keys and values', async () => { - await ccl.add('first', 'word!!') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { first: 'word!!' }) - }) - - it('handles two entries', async () => { - await ccl.add('first', 'uno') - await ccl.add('second', 'dos') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - assert.deepEqual(decoded, { - first: 'uno', - second: 'dos', - }) - }) - }) - - describe('#addBytes', () => { - it('stores and logs keys and values', async () => { - await ccl.addBytes('first', '0xaabbccddeeff') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - const expected = hexToBuf('0xaabbccddeeff') - assert.deepEqual(decoded, { first: expected }) - }) - - it('handles two entries', async () => { - await ccl.addBytes('first', '0x756E6F') - await ccl.addBytes('second', '0x646F73') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - const expectedFirst = hexToBuf('0x756E6F') - const expectedSecond = hexToBuf('0x646F73') - assert.deepEqual(decoded, { - first: expectedFirst, - second: expectedSecond, - }) - }) - - it('handles strings', async () => { - await ccl.addBytes('first', ethers.utils.toUtf8Bytes('apple')) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - const expected = ethers.utils.toUtf8Bytes('apple') - assert.deepEqual(decoded, { first: expected }) - }) - }) - - describe('#addInt', () => { - it('stores and logs keys and values', async () => { - await ccl.addInt('first', 1) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { first: 1 }) - }) - - it('handles two entries', async () => { - await ccl.addInt('first', 1) - await ccl.addInt('second', 2) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - assert.deepEqual(decoded, { - first: 1, - second: 2, - }) - }) - }) - - describe('#addUint', () => { - it('stores and logs keys and values', async () => { - await ccl.addUint('first', 1) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { first: 1 }) - }) - - it('handles two entries', async () => { - await ccl.addUint('first', 1) - await ccl.addUint('second', 2) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - assert.deepEqual(decoded, { - first: 1, - second: 2, - }) - }) - }) - - describe('#addStringArray', () => { - it('stores and logs keys and values', async () => { - await ccl.addStringArray('word', [ - ethers.utils.formatBytes32String('seinfeld'), - ethers.utils.formatBytes32String('"4"'), - ethers.utils.formatBytes32String('LIFE'), - ]) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { word: ['seinfeld', '"4"', 'LIFE'] }) - }) - }) -}) diff --git a/contracts/test/v0.6/ChainlinkClient.test.ts b/contracts/test/v0.6/ChainlinkClient.test.ts deleted file mode 100644 index bfd43d7f3f9..00000000000 --- a/contracts/test/v0.6/ChainlinkClient.test.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { ethers } from 'hardhat' -import { assert } from 'chai' -import { Contract, ContractFactory } from 'ethers' -import { Roles, getUsers } from '../test-helpers/setup' -import { - convertFufillParams, - decodeCCRequest, - decodeRunRequest, - RunRequest, -} from '../test-helpers/oracle' -import { decodeDietCBOR } from '../test-helpers/helpers' -import { evmRevert } from '../test-helpers/matchers' - -let concreteChainlinkClientFactory: ContractFactory -let emptyOracleFactory: ContractFactory -let getterSetterFactory: ContractFactory -let oracleFactory: ContractFactory -let linkTokenFactory: ContractFactory - -let roles: Roles - -before(async () => { - roles = (await getUsers()).roles - - concreteChainlinkClientFactory = await ethers.getContractFactory( - 'src/v0.6/tests/ChainlinkClientTestHelper.sol:ChainlinkClientTestHelper', - roles.defaultAccount, - ) - emptyOracleFactory = await ethers.getContractFactory( - 'src/v0.6/tests/EmptyOracle.sol:EmptyOracle', - roles.defaultAccount, - ) - getterSetterFactory = await ethers.getContractFactory( - 'src/v0.5/tests/GetterSetter.sol:GetterSetter', - roles.defaultAccount, - ) - oracleFactory = await ethers.getContractFactory( - 'src/v0.4/Oracle.sol:Oracle', - roles.defaultAccount, - ) - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) -}) - -describe('ChainlinkClientTestHelper', () => { - const specId = - '0x4c7b7ffb66b344fbaa64995af81e355a00000000000000000000000000000000' - let cc: Contract - let gs: Contract - let oc: Contract - let newoc: Contract - let link: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - oc = await oracleFactory.connect(roles.defaultAccount).deploy(link.address) - newoc = await oracleFactory - .connect(roles.defaultAccount) - .deploy(link.address) - gs = await getterSetterFactory.connect(roles.defaultAccount).deploy() - cc = await concreteChainlinkClientFactory - .connect(roles.defaultAccount) - .deploy(link.address, oc.address) - }) - - describe('#newRequest', () => { - it('forwards the information to the oracle contract through the link token', async () => { - const tx = await cc.publicNewRequest( - specId, - gs.address, - ethers.utils.toUtf8Bytes('requestedBytes32(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - - assert.equal(1, receipt.logs?.length) - const [jId, cbAddr, cbFId, cborData] = receipt.logs - ? decodeCCRequest(receipt.logs[0]) - : [] - const params = decodeDietCBOR(cborData ?? '') - - assert.equal(specId, jId) - assert.equal(gs.address, cbAddr) - assert.equal('0xed53e511', cbFId) - assert.deepEqual({}, params) - }) - }) - - describe('#chainlinkRequest(Request)', () => { - it('emits an event from the contract showing the run ID', async () => { - const tx = await cc.publicRequest( - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - - const { events, logs } = await tx.wait() - - assert.equal(4, events?.length) - - assert.equal(logs?.[0].address, cc.address) - assert.equal(events?.[0].event, 'ChainlinkRequested') - }) - }) - - describe('#chainlinkRequestTo(Request)', () => { - it('emits an event from the contract showing the run ID', async () => { - const tx = await cc.publicRequestRunTo( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { events } = await tx.wait() - - assert.equal(4, events?.length) - assert.equal(events?.[0].event, 'ChainlinkRequested') - }) - - it('emits an event on the target oracle contract', async () => { - const tx = await cc.publicRequestRunTo( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { logs } = await tx.wait() - const event = logs && newoc.interface.parseLog(logs[3]) - - assert.equal(4, logs?.length) - assert.equal(event?.name, 'OracleRequest') - }) - - it('does not modify the stored oracle address', async () => { - await cc.publicRequestRunTo( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - - const actualOracleAddress = await cc.publicOracleAddress() - assert.equal(oc.address, actualOracleAddress) - }) - }) - - describe('#cancelChainlinkRequest', () => { - let requestId: string - // a concrete chainlink attached to an empty oracle - let ecc: Contract - - beforeEach(async () => { - const emptyOracle = await emptyOracleFactory - .connect(roles.defaultAccount) - .deploy() - ecc = await concreteChainlinkClientFactory - .connect(roles.defaultAccount) - .deploy(link.address, emptyOracle.address) - - const tx = await ecc.publicRequest( - specId, - ecc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { events } = await tx.wait() - requestId = (events?.[0]?.args as any).id - }) - - it('emits an event from the contract showing the run was cancelled', async () => { - const tx = await ecc.publicCancelRequest( - requestId, - 0, - ethers.utils.hexZeroPad('0x', 4), - 0, - ) - const { events } = await tx.wait() - - assert.equal(1, events?.length) - assert.equal(events?.[0].event, 'ChainlinkCancelled') - assert.equal(requestId, (events?.[0].args as any).id) - }) - - it('throws if given a bogus event ID', async () => { - await evmRevert( - ecc.publicCancelRequest( - ethers.utils.formatBytes32String('bogusId'), - 0, - ethers.utils.hexZeroPad('0x', 4), - 0, - ), - ) - }) - }) - - describe('#recordChainlinkFulfillment(modifier)', () => { - let request: RunRequest - - beforeEach(async () => { - const tx = await cc.publicRequest( - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { logs } = await tx.wait() - - request = decodeRunRequest(logs?.[3]) - }) - - it('emits an event marking the request fulfilled', async () => { - const tx = await oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - const { logs } = await tx.wait() - - const event = logs && cc.interface.parseLog(logs[0]) - - assert.equal(1, logs?.length) - assert.equal(event?.name, 'ChainlinkFulfilled') - assert.equal(request.requestId, event?.args.id) - }) - - it('should only allow one fulfillment per id', async () => { - await oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - - await evmRevert( - oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Must have a valid requestId', - ) - }) - - it('should only allow the oracle to fulfill the request', async () => { - await evmRevert( - oc - .connect(roles.stranger) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Not an authorized node to fulfill requests', - ) - }) - }) - - describe('#fulfillChainlinkRequest(function)', () => { - let request: RunRequest - - beforeEach(async () => { - const tx = await cc.publicRequest( - specId, - cc.address, - ethers.utils.toUtf8Bytes( - 'publicFulfillChainlinkRequest(bytes32,bytes32)', - ), - 0, - ) - const { logs } = await tx.wait() - - request = decodeRunRequest(logs?.[3]) - }) - - it('emits an event marking the request fulfilled', async () => { - const tx = await oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - - const { logs } = await tx.wait() - const event = logs && cc.interface.parseLog(logs[0]) - - assert.equal(1, logs?.length) - assert.equal(event?.name, 'ChainlinkFulfilled') - assert.equal(request.requestId, event?.args?.id) - }) - - it('should only allow one fulfillment per id', async () => { - await oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - - await evmRevert( - oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Must have a valid requestId', - ) - }) - - it('should only allow the oracle to fulfill the request', async () => { - await evmRevert( - oc - .connect(roles.stranger) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Not an authorized node to fulfill requests', - ) - }) - }) - - describe('#chainlinkToken', () => { - it('returns the Link Token address', async () => { - const addr = await cc.publicChainlinkToken() - assert.equal(addr, link.address) - }) - }) - - describe('#addExternalRequest', () => { - let mock: Contract - let request: RunRequest - - beforeEach(async () => { - mock = await concreteChainlinkClientFactory - .connect(roles.defaultAccount) - .deploy(link.address, oc.address) - - const tx = await cc.publicRequest( - specId, - mock.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const receipt = await tx.wait() - - request = decodeRunRequest(receipt.logs?.[3]) - await mock.publicAddExternalRequest(oc.address, request.requestId) - }) - - it('allows the external request to be fulfilled', async () => { - await oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - }) - - it('does not allow the same requestId to be used', async () => { - await evmRevert( - cc.publicAddExternalRequest(newoc.address, request.requestId), - ) - }) - }) -}) diff --git a/contracts/test/v0.6/CheckedMath.test.ts b/contracts/test/v0.6/CheckedMath.test.ts deleted file mode 100644 index 14520d9d9b9..00000000000 --- a/contracts/test/v0.6/CheckedMath.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-License-Identifier: MIT -// Adapted from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c9630526e24ba53d9647787588a19ffaa3dd65e1/test/math/SignedSafeMath.test.js - -import { ethers } from 'hardhat' -import { assert } from 'chai' -import { BigNumber, constants, Contract, ContractFactory } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { bigNumEquals } from '../test-helpers/matchers' - -let mathFactory: ContractFactory -let personas: Personas - -before(async () => { - personas = (await getUsers()).personas - mathFactory = await ethers.getContractFactory( - 'src/v0.6/tests/CheckedMathTestHelper.sol:CheckedMathTestHelper', - personas.Default, - ) -}) - -const int256Max = constants.MaxInt256 -const int256Min = constants.MinInt256 - -describe('CheckedMath', () => { - let math: Contract - - beforeEach(async () => { - math = await mathFactory.connect(personas.Default).deploy() - }) - - describe('#add', () => { - const a = BigNumber.from('1234') - const b = BigNumber.from('5678') - - it('is commutative', async () => { - const c1 = await math.add(a, b) - const c2 = await math.add(b, a) - - bigNumEquals(c1.result, c2.result) - assert.isTrue(c1.ok) - assert.isTrue(c2.ok) - }) - - it('is commutative with big numbers', async () => { - const c1 = await math.add(int256Max, int256Min) - const c2 = await math.add(int256Min, int256Max) - - bigNumEquals(c1.result, c2.result) - assert.isTrue(c1.ok) - assert.isTrue(c2.ok) - }) - - it('returns false when overflowing', async () => { - const c1 = await math.add(int256Max, 1) - const c2 = await math.add(1, int256Max) - - bigNumEquals(0, c1.result) - bigNumEquals(0, c2.result) - assert.isFalse(c1.ok) - assert.isFalse(c2.ok) - }) - - it('returns false when underflowing', async () => { - const c1 = await math.add(int256Min, -1) - const c2 = await math.add(-1, int256Min) - - bigNumEquals(0, c1.result) - bigNumEquals(0, c2.result) - assert.isFalse(c1.ok) - assert.isFalse(c2.ok) - }) - }) - - describe('#sub', () => { - const a = BigNumber.from('1234') - const b = BigNumber.from('5678') - - it('subtracts correctly if it does not overflow and the result is negative', async () => { - const c = await math.sub(a, b) - const expected = a.sub(b) - - bigNumEquals(expected, c.result) - assert.isTrue(c.ok) - }) - - it('subtracts correctly if it does not overflow and the result is positive', async () => { - const c = await math.sub(b, a) - const expected = b.sub(a) - - bigNumEquals(expected, c.result) - assert.isTrue(c.ok) - }) - - it('returns false on overflow', async () => { - const c = await math.sub(int256Max, -1) - - bigNumEquals(0, c.result) - assert.isFalse(c.ok) - }) - - it('returns false on underflow', async () => { - const c = await math.sub(int256Min, 1) - - bigNumEquals(0, c.result) - assert.isFalse(c.ok) - }) - }) - - describe('#mul', () => { - const a = BigNumber.from('5678') - const b = BigNumber.from('-1234') - - it('is commutative', async () => { - const c1 = await math.mul(a, b) - const c2 = await math.mul(b, a) - - bigNumEquals(c1.result, c2.result) - assert.isTrue(c1.ok) - assert.isTrue(c2.ok) - }) - - it('multiplies by 0 correctly', async () => { - const c = await math.mul(a, 0) - - bigNumEquals(0, c.result) - assert.isTrue(c.ok) - }) - - it('returns false on multiplication overflow', async () => { - const c = await math.mul(int256Max, 2) - - bigNumEquals(0, c.result) - assert.isFalse(c.ok) - }) - - it('returns false when the integer minimum is negated', async () => { - const c = await math.mul(int256Min, -1) - - bigNumEquals(0, c.result) - assert.isFalse(c.ok) - }) - }) - - describe('#div', () => { - const a = BigNumber.from('5678') - const b = BigNumber.from('-5678') - - it('divides correctly', async () => { - const c = await math.div(a, b) - - bigNumEquals(a.div(b), c.result) - assert.isTrue(c.ok) - }) - - it('divides a 0 numerator correctly', async () => { - const c = await math.div(0, a) - - bigNumEquals(0, c.result) - assert.isTrue(c.ok) - }) - - it('returns complete number result on non-even division', async () => { - const c = await math.div(7000, 5678) - - bigNumEquals(1, c.result) - assert.isTrue(c.ok) - }) - - it('reverts when 0 is the denominator', async () => { - const c = await math.div(a, 0) - - bigNumEquals(0, c.result) - assert.isFalse(c.ok) - }) - - it('reverts on underflow with a negative denominator', async () => { - const c = await math.div(int256Min, -1) - - bigNumEquals(0, c.result) - assert.isFalse(c.ok) - }) - }) -}) diff --git a/contracts/test/v0.6/DeviationFlaggingValidator.test.ts b/contracts/test/v0.6/DeviationFlaggingValidator.test.ts deleted file mode 100644 index f79a8c7aa47..00000000000 --- a/contracts/test/v0.6/DeviationFlaggingValidator.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { BigNumber, Contract, ContractFactory } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { bigNumEquals } from '../test-helpers/matchers' - -let personas: Personas -let validatorFactory: ContractFactory -let flagsFactory: ContractFactory -let acFactory: ContractFactory - -before(async () => { - personas = (await getUsers()).personas - validatorFactory = await ethers.getContractFactory( - 'src/v0.6/DeviationFlaggingValidator.sol:DeviationFlaggingValidator', - personas.Carol, - ) - flagsFactory = await ethers.getContractFactory( - 'src/v0.6/Flags.sol:Flags', - personas.Carol, - ) - acFactory = await ethers.getContractFactory( - 'src/v0.6/SimpleWriteAccessController.sol:SimpleWriteAccessController', - personas.Carol, - ) -}) - -describe('DeviationFlaggingValidator', () => { - let validator: Contract - let flags: Contract - let ac: Contract - const flaggingThreshold = 10000 // 10% - const previousRoundId = 2 - const previousValue = 1000000 - const currentRoundId = 3 - const currentValue = 1000000 - - beforeEach(async () => { - ac = await acFactory.connect(personas.Carol).deploy() - flags = await flagsFactory.connect(personas.Carol).deploy(ac.address) - validator = await validatorFactory - .connect(personas.Carol) - .deploy(flags.address, flaggingThreshold) - await ac.connect(personas.Carol).addAccess(validator.address) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(validator, [ - 'THRESHOLD_MULTIPLIER', - 'flaggingThreshold', - 'flags', - 'isValid', - 'setFlagsAddress', - 'setFlaggingThreshold', - 'validate', - // Owned methods: - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('#constructor', () => { - it('sets the arguments passed in', async () => { - assert.equal(flags.address, await validator.flags()) - bigNumEquals(flaggingThreshold, await validator.flaggingThreshold()) - }) - }) - - describe('#validate', () => { - describe('when the deviation is greater than the threshold', () => { - const currentValue = 1100010 - - it('does raises a flag for the calling address', async () => { - await expect( - validator - .connect(personas.Nelly) - .validate( - previousRoundId, - previousValue, - currentRoundId, - currentValue, - ), - ) - .to.emit(flags, 'FlagRaised') - .withArgs(await personas.Nelly.getAddress()) - }) - - it('uses less than the gas allotted by the aggregator', async () => { - const tx = await validator - .connect(personas.Nelly) - .validate( - previousRoundId, - previousValue, - currentRoundId, - currentValue, - ) - const receipt = await tx.wait() - assert(receipt) - if (receipt && receipt.gasUsed) { - assert.isAbove(receipt.gasUsed.toNumber(), 60000) - } - }) - }) - - describe('when the deviation is less than or equal to the threshold', () => { - const currentValue = 1100009 - - it('does raises a flag for the calling address', async () => { - await expect( - validator - .connect(personas.Nelly) - .validate( - previousRoundId, - previousValue, - currentRoundId, - currentValue, - ), - ).to.not.emit(flags, 'FlagRaised') - }) - - it('uses less than the gas allotted by the aggregator', async () => { - const tx = await validator - .connect(personas.Nelly) - .validate( - previousRoundId, - previousValue, - currentRoundId, - currentValue, - ) - const receipt = await tx.wait() - assert(receipt) - if (receipt && receipt.gasUsed) { - assert.isAbove(receipt.gasUsed.toNumber(), 24000) - } - }) - }) - - describe('when called with a previous value of zero', () => { - const previousValue = 0 - - it('does not raise any flags', async () => { - const tx = await validator - .connect(personas.Nelly) - .validate( - previousRoundId, - previousValue, - currentRoundId, - currentValue, - ) - const receipt = await tx.wait() - assert.equal(0, receipt.events?.length) - }) - }) - }) - - describe('#isValid', () => { - const previousValue = 1000000 - - describe('with a validation larger than the deviation', () => { - const currentValue = 1100010 - it('is not valid', async () => { - assert.isFalse( - await validator.isValid(0, previousValue, 1, currentValue), - ) - }) - }) - - describe('with a validation smaller than the deviation', () => { - const currentValue = 1100009 - it('is valid', async () => { - assert.isTrue( - await validator.isValid(0, previousValue, 1, currentValue), - ) - }) - }) - - describe('with positive previous and negative current', () => { - const previousValue = 1000000 - const currentValue = -900000 - it('correctly detects the difference', async () => { - assert.isFalse( - await validator.isValid(0, previousValue, 1, currentValue), - ) - }) - }) - - describe('with negative previous and positive current', () => { - const previousValue = -900000 - const currentValue = 1000000 - it('correctly detects the difference', async () => { - assert.isFalse( - await validator.isValid(0, previousValue, 1, currentValue), - ) - }) - }) - - describe('when the difference overflows', () => { - const previousValue = BigNumber.from(2).pow(255).sub(1) - const currentValue = BigNumber.from(-1) - - it('does not revert and returns false', async () => { - assert.isFalse( - await validator.isValid(0, previousValue, 1, currentValue), - ) - }) - }) - - describe('when the rounding overflows', () => { - const previousValue = BigNumber.from(2).pow(255).div(10000) - const currentValue = BigNumber.from(1) - - it('does not revert and returns false', async () => { - assert.isFalse( - await validator.isValid(0, previousValue, 1, currentValue), - ) - }) - }) - - describe('when the division overflows', () => { - const previousValue = BigNumber.from(2).pow(255).sub(1) - const currentValue = BigNumber.from(-1) - - it('does not revert and returns false', async () => { - assert.isFalse( - await validator.isValid(0, previousValue, 1, currentValue), - ) - }) - }) - }) - - describe('#setFlaggingThreshold', () => { - const newThreshold = 777 - - it('changes the flagging thresold', async () => { - assert.equal(flaggingThreshold, await validator.flaggingThreshold()) - - await validator.connect(personas.Carol).setFlaggingThreshold(newThreshold) - - assert.equal(newThreshold, await validator.flaggingThreshold()) - }) - - it('emits a log event only when actually changed', async () => { - await expect( - validator.connect(personas.Carol).setFlaggingThreshold(newThreshold), - ) - .to.emit(validator, 'FlaggingThresholdUpdated') - .withArgs(flaggingThreshold, newThreshold) - - await expect( - validator.connect(personas.Carol).setFlaggingThreshold(newThreshold), - ).to.not.emit(validator, 'FlaggingThresholdUpdated') - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - validator.connect(personas.Neil).setFlaggingThreshold(newThreshold), - ).to.be.revertedWith('Only callable by owner') - }) - }) - }) - - describe('#setFlagsAddress', () => { - const newFlagsAddress = '0x0123456789012345678901234567890123456789' - - it('changes the flags address', async () => { - assert.equal(flags.address, await validator.flags()) - - await validator.connect(personas.Carol).setFlagsAddress(newFlagsAddress) - - assert.equal(newFlagsAddress, await validator.flags()) - }) - - it('emits a log event only when actually changed', async () => { - await expect( - validator.connect(personas.Carol).setFlagsAddress(newFlagsAddress), - ) - .to.emit(validator, 'FlagsAddressUpdated') - .withArgs(flags.address, newFlagsAddress) - - await expect( - validator.connect(personas.Carol).setFlagsAddress(newFlagsAddress), - ).to.not.emit(validator, 'FlagsAddressUpdated') - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - validator.connect(personas.Neil).setFlagsAddress(newFlagsAddress), - ).to.be.revertedWith('Only callable by owner') - }) - }) - }) -}) diff --git a/contracts/test/v0.6/Flags.test.ts b/contracts/test/v0.6/Flags.test.ts deleted file mode 100644 index 8f589184299..00000000000 --- a/contracts/test/v0.6/Flags.test.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { Contract, ContractFactory } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' - -let personas: Personas - -let controllerFactory: ContractFactory -let flagsFactory: ContractFactory -let consumerFactory: ContractFactory - -let controller: Contract -let flags: Contract -let consumer: Contract - -before(async () => { - personas = (await getUsers()).personas - controllerFactory = await ethers.getContractFactory( - 'src/v0.6/SimpleWriteAccessController.sol:SimpleWriteAccessController', - personas.Nelly, - ) - consumerFactory = await ethers.getContractFactory( - 'src/v0.6/tests/FlagsTestHelper.sol:FlagsTestHelper', - personas.Nelly, - ) - flagsFactory = await ethers.getContractFactory( - 'src/v0.6/Flags.sol:Flags', - personas.Nelly, - ) -}) - -describe('Flags', () => { - beforeEach(async () => { - controller = await controllerFactory.deploy() - flags = await flagsFactory.deploy(controller.address) - await flags.disableAccessCheck() - consumer = await consumerFactory.deploy(flags.address) - }) - - it('has a limited public interface [ @skip-coverage ]', async () => { - publicAbi(flags, [ - 'getFlag', - 'getFlags', - 'lowerFlags', - 'raiseFlag', - 'raiseFlags', - 'raisingAccessController', - 'setRaisingAccessController', - // Ownable methods: - 'acceptOwnership', - 'owner', - 'transferOwnership', - // AccessControl methods: - 'addAccess', - 'disableAccessCheck', - 'enableAccessCheck', - 'removeAccess', - 'checkEnabled', - 'hasAccess', - ]) - }) - - describe('#raiseFlag', () => { - describe('when called by the owner', () => { - it('updates the warning flag', async () => { - assert.equal(false, await flags.getFlag(consumer.address)) - - await flags.connect(personas.Nelly).raiseFlag(consumer.address) - - assert.equal(true, await flags.getFlag(consumer.address)) - }) - - it('emits an event log', async () => { - await expect(flags.connect(personas.Nelly).raiseFlag(consumer.address)) - .to.emit(flags, 'FlagRaised') - .withArgs(consumer.address) - }) - - describe('if a flag has already been raised', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).raiseFlag(consumer.address) - }) - - it('emits an event log', async () => { - const tx = await flags - .connect(personas.Nelly) - .raiseFlag(consumer.address) - const receipt = await tx.wait() - assert.equal(0, receipt.events?.length) - }) - }) - }) - - describe('when called by an enabled setter', () => { - beforeEach(async () => { - await controller - .connect(personas.Nelly) - .addAccess(await personas.Neil.getAddress()) - }) - - it('sets the flags', async () => { - await flags.connect(personas.Neil).raiseFlag(consumer.address), - assert.equal(true, await flags.getFlag(consumer.address)) - }) - }) - - describe('when called by a non-enabled setter', () => { - it('reverts', async () => { - await expect( - flags.connect(personas.Neil).raiseFlag(consumer.address), - ).to.be.revertedWith('Not allowed to raise flags') - }) - }) - - describe('when called when there is no raisingAccessController', () => { - beforeEach(async () => { - await expect( - flags - .connect(personas.Nelly) - .setRaisingAccessController( - '0x0000000000000000000000000000000000000000', - ), - ).to.emit(flags, 'RaisingAccessControllerUpdated') - assert.equal( - '0x0000000000000000000000000000000000000000', - await flags.raisingAccessController(), - ) - }) - - it('succeeds for the owner', async () => { - await flags.connect(personas.Nelly).raiseFlag(consumer.address) - assert.equal(true, await flags.getFlag(consumer.address)) - }) - - it('reverts for non-owner', async () => { - await expect(flags.connect(personas.Neil).raiseFlag(consumer.address)) - .to.be.reverted - }) - }) - }) - - describe('#raiseFlags', () => { - describe('when called by the owner', () => { - it('updates the warning flag', async () => { - assert.equal(false, await flags.getFlag(consumer.address)) - - await flags.connect(personas.Nelly).raiseFlags([consumer.address]) - - assert.equal(true, await flags.getFlag(consumer.address)) - }) - - it('emits an event log', async () => { - await expect( - flags.connect(personas.Nelly).raiseFlags([consumer.address]), - ) - .to.emit(flags, 'FlagRaised') - .withArgs(consumer.address) - }) - - describe('if a flag has already been raised', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).raiseFlags([consumer.address]) - }) - - it('emits an event log', async () => { - const tx = await flags - .connect(personas.Nelly) - .raiseFlags([consumer.address]) - const receipt = await tx.wait() - assert.equal(0, receipt.events?.length) - }) - }) - }) - - describe('when called by an enabled setter', () => { - beforeEach(async () => { - await controller - .connect(personas.Nelly) - .addAccess(await personas.Neil.getAddress()) - }) - - it('sets the flags', async () => { - await flags.connect(personas.Neil).raiseFlags([consumer.address]), - assert.equal(true, await flags.getFlag(consumer.address)) - }) - }) - - describe('when called by a non-enabled setter', () => { - it('reverts', async () => { - await expect( - flags.connect(personas.Neil).raiseFlags([consumer.address]), - ).to.be.revertedWith('Not allowed to raise flags') - }) - }) - - describe('when called when there is no raisingAccessController', () => { - beforeEach(async () => { - await expect( - flags - .connect(personas.Nelly) - .setRaisingAccessController( - '0x0000000000000000000000000000000000000000', - ), - ).to.emit(flags, 'RaisingAccessControllerUpdated') - - assert.equal( - '0x0000000000000000000000000000000000000000', - await flags.raisingAccessController(), - ) - }) - - it('succeeds for the owner', async () => { - await flags.connect(personas.Nelly).raiseFlags([consumer.address]) - assert.equal(true, await flags.getFlag(consumer.address)) - }) - - it('reverts for non-owners', async () => { - await expect( - flags.connect(personas.Neil).raiseFlags([consumer.address]), - ).to.be.reverted - }) - }) - }) - - describe('#lowerFlags', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).raiseFlags([consumer.address]) - }) - - describe('when called by the owner', () => { - it('updates the warning flag', async () => { - assert.equal(true, await flags.getFlag(consumer.address)) - - await flags.connect(personas.Nelly).lowerFlags([consumer.address]) - - assert.equal(false, await flags.getFlag(consumer.address)) - }) - - it('emits an event log', async () => { - await expect( - flags.connect(personas.Nelly).lowerFlags([consumer.address]), - ) - .to.emit(flags, 'FlagLowered') - .withArgs(consumer.address) - }) - - describe('if a flag has already been raised', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).lowerFlags([consumer.address]) - }) - - it('emits an event log', async () => { - const tx = await flags - .connect(personas.Nelly) - .lowerFlags([consumer.address]) - const receipt = await tx.wait() - assert.equal(0, receipt.events?.length) - }) - }) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - flags.connect(personas.Neil).lowerFlags([consumer.address]), - ).to.be.revertedWith('Only callable by owner') - }) - }) - }) - - describe('#getFlag', () => { - describe('if the access control is turned on', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).enableAccessCheck() - }) - - it('reverts', async () => { - await expect(consumer.getFlag(consumer.address)).to.be.revertedWith( - 'No access', - ) - }) - - describe('if access is granted to the address', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).addAccess(consumer.address) - }) - - it('does not revert', async () => { - await consumer.getFlag(consumer.address) - }) - }) - }) - - describe('if the access control is turned off', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).disableAccessCheck() - }) - - it('does not revert', async () => { - await consumer.getFlag(consumer.address) - }) - - describe('if access is granted to the address', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).addAccess(consumer.address) - }) - - it('does not revert', async () => { - await consumer.getFlag(consumer.address) - }) - }) - }) - }) - - describe('#getFlags', () => { - beforeEach(async () => { - await flags.connect(personas.Nelly).disableAccessCheck() - await flags - .connect(personas.Nelly) - .raiseFlags([ - await personas.Neil.getAddress(), - await personas.Norbert.getAddress(), - ]) - }) - - it('respects the access controls of #getFlag', async () => { - await flags.connect(personas.Nelly).enableAccessCheck() - - await expect(consumer.getFlag(consumer.address)).to.be.revertedWith( - 'No access', - ) - - await flags.connect(personas.Nelly).addAccess(consumer.address) - - await consumer.getFlag(consumer.address) - }) - - it('returns the flags in the order they are requested', async () => { - const response = await consumer.getFlags([ - await personas.Nelly.getAddress(), - await personas.Neil.getAddress(), - await personas.Ned.getAddress(), - await personas.Norbert.getAddress(), - ]) - - assert.deepEqual([false, true, false, true], response) - }) - }) - - describe('#setRaisingAccessController', () => { - let controller2: Contract - - beforeEach(async () => { - controller2 = await controllerFactory.connect(personas.Nelly).deploy() - await controller2.connect(personas.Nelly).enableAccessCheck() - }) - - it('updates access control rules', async () => { - const neilAddress = await personas.Neil.getAddress() - await controller.connect(personas.Nelly).addAccess(neilAddress) - await flags.connect(personas.Neil).raiseFlags([consumer.address]) // doesn't raise - - await flags - .connect(personas.Nelly) - .setRaisingAccessController(controller2.address) - - await expect( - flags.connect(personas.Neil).raiseFlags([consumer.address]), - ).to.be.revertedWith('Not allowed to raise flags') - }) - - it('emits a log announcing the change', async () => { - await expect( - flags - .connect(personas.Nelly) - .setRaisingAccessController(controller2.address), - ) - .to.emit(flags, 'RaisingAccessControllerUpdated') - .withArgs(controller.address, controller2.address) - }) - - it('does not emit a log when there is no change', async () => { - await flags - .connect(personas.Nelly) - .setRaisingAccessController(controller2.address) - - await expect( - flags - .connect(personas.Nelly) - .setRaisingAccessController(controller2.address), - ).to.not.emit(flags, 'RaisingAccessControllerUpdated') - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - flags - .connect(personas.Neil) - .setRaisingAccessController(controller2.address), - ).to.be.revertedWith('Only callable by owner') - }) - }) - }) -}) diff --git a/contracts/test/v0.6/FluxAggregator.test.ts b/contracts/test/v0.6/FluxAggregator.test.ts deleted file mode 100644 index 5a268ceebe9..00000000000 --- a/contracts/test/v0.6/FluxAggregator.test.ts +++ /dev/null @@ -1,3252 +0,0 @@ -import { ethers } from 'hardhat' -import { assert, expect } from 'chai' -import { - Signer, - Contract, - ContractFactory, - BigNumber, - BigNumberish, - ContractTransaction, - constants, -} from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { bigNumEquals, evmRevert } from '../test-helpers/matchers' -import { - publicAbi, - toWei, - increaseTimeBy, - mineBlock, - evmWordToAddress, -} from '../test-helpers/helpers' -import { randomBytes } from '@ethersproject/random' -import { fail } from 'assert' - -let personas: Personas -let linkTokenFactory: ContractFactory -let fluxAggregatorFactory: ContractFactory -let validatorMockFactory: ContractFactory -let testHelperFactory: ContractFactory -let validatorFactory: ContractFactory -let flagsFactory: ContractFactory -let acFactory: ContractFactory -let gasGuzzlerFactory: ContractFactory -let emptyAddress: string - -before(async () => { - personas = (await getUsers()).personas - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - ) - fluxAggregatorFactory = await ethers.getContractFactory( - 'src/v0.6/FluxAggregator.sol:FluxAggregator', - ) - validatorMockFactory = await ethers.getContractFactory( - 'src/v0.6/tests/AggregatorValidatorMock.sol:AggregatorValidatorMock', - ) - testHelperFactory = await ethers.getContractFactory( - 'src/v0.6/tests/FluxAggregatorTestHelper.sol:FluxAggregatorTestHelper', - ) - validatorFactory = await ethers.getContractFactory( - 'src/v0.6/DeviationFlaggingValidator.sol:DeviationFlaggingValidator', - ) - flagsFactory = await ethers.getContractFactory('src/v0.6/Flags.sol:Flags') - acFactory = await ethers.getContractFactory( - 'src/v0.6/SimpleWriteAccessController.sol:SimpleWriteAccessController', - ) - gasGuzzlerFactory = await ethers.getContractFactory( - 'src/v0.6/tests/GasGuzzler.sol:GasGuzzler', - ) - emptyAddress = constants.AddressZero -}) - -describe('FluxAggregator', () => { - const paymentAmount = toWei('3') - const deposit = toWei('100') - const answer = 100 - const minAns = 1 - const maxAns = 1 - const rrDelay = 0 - const timeout = 1800 - const decimals = 18 - const description = 'LINK/USD' - const reserveRounds = 2 - const minSubmissionValue = BigNumber.from('1') - const maxSubmissionValue = BigNumber.from('100000000000000000000') - - let aggregator: Contract - let link: Contract - let testHelper: Contract - let validator: Contract - let gasGuzzler: Contract - let nextRound: number - let oracles: Signer[] - - async function updateFutureRounds( - aggregator: Contract, - overrides: { - minAnswers?: BigNumberish - maxAnswers?: BigNumberish - payment?: BigNumberish - restartDelay?: BigNumberish - timeout?: BigNumberish - } = {}, - ) { - overrides = overrides || {} - const round = { - payment: overrides.payment || paymentAmount, - minAnswers: overrides.minAnswers || minAns, - maxAnswers: overrides.maxAnswers || maxAns, - restartDelay: overrides.restartDelay || rrDelay, - timeout: overrides.timeout || timeout, - } - - return aggregator.updateFutureRounds( - round.payment, - round.minAnswers, - round.maxAnswers, - round.restartDelay, - round.timeout, - ) - } - - async function addOracles( - aggregator: Contract, - oraclesAndAdmin: Signer[], - minAnswers: number, - maxAnswers: number, - restartDelay: number, - ): Promise { - return aggregator.connect(personas.Carol).changeOracles( - [], - oraclesAndAdmin.map(async (oracle) => await oracle.getAddress()), - oraclesAndAdmin.map(async (admin) => await admin.getAddress()), - minAnswers, - maxAnswers, - restartDelay, - ) - } - - async function advanceRound( - aggregator: Contract, - submitters: Signer[], - currentSubmission: number = answer, - ): Promise { - for (const submitter of submitters) { - await aggregator.connect(submitter).submit(nextRound, currentSubmission) - } - nextRound++ - return nextRound - } - - const ShouldBeSet = 'expects it to be different' - const ShouldNotBeSet = 'expects it to equal' - let startingState: any - - async function checkOracleRoundState( - state: any, - want: { - eligibleToSubmit: boolean - roundId: BigNumberish - latestSubmission: BigNumberish - startedAt: string - timeout: BigNumberish - availableFunds: BigNumberish - oracleCount: BigNumberish - paymentAmount: BigNumberish - }, - ) { - assert.equal( - want.eligibleToSubmit, - state._eligibleToSubmit, - 'round state: unexecpted eligibility', - ) - bigNumEquals( - want.roundId, - state._roundId, - 'round state: unexpected Round ID', - ) - bigNumEquals( - want.latestSubmission, - state._latestSubmission, - 'round state: unexpected latest submission', - ) - if (want.startedAt === ShouldBeSet) { - assert.isAbove( - state._startedAt.toNumber(), - startingState._startedAt.toNumber(), - 'round state: expected the started at to be the same as previous', - ) - } else { - bigNumEquals( - 0, - state._startedAt, - 'round state: expected the started at not to be updated', - ) - } - bigNumEquals( - want.timeout, - state._timeout.toNumber(), - 'round state: unexepcted timeout', - ) - bigNumEquals( - want.availableFunds, - state._availableFunds, - 'round state: unexepected funds', - ) - bigNumEquals( - want.oracleCount, - state._oracleCount, - 'round state: unexpected oracle count', - ) - bigNumEquals( - want.paymentAmount, - state._paymentAmount, - 'round state: unexpected paymentamount', - ) - } - - beforeEach(async () => { - link = await linkTokenFactory.connect(personas.Default).deploy() - aggregator = await fluxAggregatorFactory - .connect(personas.Carol) - .deploy( - link.address, - paymentAmount, - timeout, - emptyAddress, - minSubmissionValue, - maxSubmissionValue, - decimals, - ethers.utils.formatBytes32String(description), - ) - await link.transfer(aggregator.address, deposit) - await aggregator.updateAvailableFunds() - bigNumEquals(deposit, await link.balanceOf(aggregator.address)) - nextRound = 1 - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(aggregator, [ - 'acceptAdmin', - 'allocatedFunds', - 'availableFunds', - 'changeOracles', - 'decimals', - 'description', - 'getAdmin', - 'getAnswer', - 'getOracles', - 'getRoundData', - 'getTimestamp', - 'latestAnswer', - 'latestRound', - 'latestRoundData', - 'latestTimestamp', - 'linkToken', - 'maxSubmissionCount', - 'maxSubmissionValue', - 'minSubmissionCount', - 'minSubmissionValue', - 'onTokenTransfer', - 'oracleCount', - 'oracleRoundState', - 'paymentAmount', - 'requestNewRound', - 'restartDelay', - 'setRequesterPermissions', - 'setValidator', - 'submit', - 'timeout', - 'transferAdmin', - 'updateAvailableFunds', - 'updateFutureRounds', - 'withdrawFunds', - 'withdrawPayment', - 'withdrawablePayment', - 'validator', - 'version', - // Owned methods: - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('#constructor', () => { - it('sets the paymentAmount', async () => { - bigNumEquals( - BigNumber.from(paymentAmount), - await aggregator.paymentAmount(), - ) - }) - - it('sets the timeout', async () => { - bigNumEquals(BigNumber.from(timeout), await aggregator.timeout()) - }) - - it('sets the decimals', async () => { - bigNumEquals(BigNumber.from(decimals), await aggregator.decimals()) - }) - - it('sets the description', async () => { - assert.equal( - ethers.utils.formatBytes32String(description), - await aggregator.description(), - ) - }) - - it('sets the version to 3', async () => { - bigNumEquals(3, await aggregator.version()) - }) - - it('sets the validator', async () => { - assert.equal(emptyAddress, await aggregator.validator()) - }) - }) - - describe('#submit', () => { - let minMax - - beforeEach(async () => { - oracles = [personas.Neil, personas.Ned, personas.Nelly] - minMax = oracles.length - await addOracles(aggregator, oracles, minMax, minMax, rrDelay) - }) - - it('updates the allocated and available funds counters', async () => { - bigNumEquals(0, await aggregator.allocatedFunds()) - - const tx = await aggregator - .connect(personas.Neil) - .submit(nextRound, answer) - const receipt = await tx.wait() - - bigNumEquals(paymentAmount, await aggregator.allocatedFunds()) - const expectedAvailable = deposit.sub(paymentAmount) - bigNumEquals(expectedAvailable, await aggregator.availableFunds()) - const logged = BigNumber.from( - receipt.logs?.[2].topics[1] ?? BigNumber.from(-1), - ) - bigNumEquals(expectedAvailable, logged) - }) - - it('emits a log event announcing submission details', async () => { - await expect(aggregator.connect(personas.Nelly).submit(nextRound, answer)) - .to.emit(aggregator, 'SubmissionReceived') - .withArgs(answer, nextRound, await personas.Nelly.getAddress()) - }) - - describe('when the minimum oracles have not reported', () => { - it('pays the oracles that have reported', async () => { - bigNumEquals( - 0, - await aggregator - .connect(personas.Neil) - .withdrawablePayment(await personas.Neil.getAddress()), - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - - bigNumEquals( - paymentAmount, - await aggregator - .connect(personas.Neil) - .withdrawablePayment(await personas.Neil.getAddress()), - ) - bigNumEquals( - 0, - await aggregator - .connect(personas.Ned) - .withdrawablePayment(await personas.Ned.getAddress()), - ) - bigNumEquals( - 0, - await aggregator - .connect(personas.Nelly) - .withdrawablePayment(await personas.Nelly.getAddress()), - ) - }) - - it('does not update the answer', async () => { - bigNumEquals(ethers.constants.Zero, await aggregator.latestAnswer()) - - // Not updated because of changes by the owner setting minSubmissionCount to 3 - await aggregator.connect(personas.Ned).submit(nextRound, answer) - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - - bigNumEquals(ethers.constants.Zero, await aggregator.latestAnswer()) - }) - }) - - describe('when an oracle prematurely bumps the round', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { minAnswers: 2, maxAnswers: 3 }) - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - - it('reverts', async () => { - await evmRevert( - aggregator.connect(personas.Neil).submit(nextRound + 1, answer), - 'previous round not supersedable', - ) - }) - }) - - describe('when the minimum number of oracles have reported', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { minAnswers: 2, maxAnswers: 3 }) - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - - it('updates the answer with the median', async () => { - bigNumEquals(0, await aggregator.latestAnswer()) - - await aggregator.connect(personas.Ned).submit(nextRound, 99) - bigNumEquals(99, await aggregator.latestAnswer()) // ((100+99) / 2).to_i - - await aggregator.connect(personas.Nelly).submit(nextRound, 101) - - bigNumEquals(100, await aggregator.latestAnswer()) - }) - - it('updates the updated timestamp', async () => { - const originalTimestamp = await aggregator.latestTimestamp() - assert.isAbove(originalTimestamp.toNumber(), 0) - - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - - const currentTimestamp = await aggregator.latestTimestamp() - assert.isAbove( - currentTimestamp.toNumber(), - originalTimestamp.toNumber(), - ) - }) - - it('announces the new answer with a log event', async () => { - const tx = await aggregator - .connect(personas.Nelly) - .submit(nextRound, answer) - const receipt = await tx.wait() - - const newAnswer = BigNumber.from( - receipt.logs?.[0].topics[1] ?? ethers.constants.Zero, - ) - - assert.equal(answer, newAnswer.toNumber()) - }) - - it('does not set the timedout flag', async () => { - evmRevert(aggregator.getRoundData(nextRound), 'No data present') - - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - - const round = await aggregator.getRoundData(nextRound) - assert.equal(nextRound, round.answeredInRound.toNumber()) - }) - - it('updates the round details', async () => { - evmRevert(aggregator.latestRoundData(), 'No data present') - - increaseTimeBy(15, ethers.provider) - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - - const roundAfter = await aggregator.getRoundData(nextRound) - bigNumEquals(nextRound, roundAfter.roundId) - bigNumEquals(answer, roundAfter.answer) - assert.isFalse(roundAfter.startedAt.isZero()) - bigNumEquals( - await aggregator.getTimestamp(nextRound), - roundAfter.updatedAt, - ) - bigNumEquals(nextRound, roundAfter.answeredInRound) - - assert.isBelow( - roundAfter.startedAt.toNumber(), - roundAfter.updatedAt.toNumber(), - ) - - const roundAfterLatest = await aggregator.latestRoundData() - bigNumEquals(roundAfter.roundId, roundAfterLatest.roundId) - bigNumEquals(roundAfter.answer, roundAfterLatest.answer) - bigNumEquals(roundAfter.startedAt, roundAfterLatest.startedAt) - bigNumEquals(roundAfter.updatedAt, roundAfterLatest.updatedAt) - bigNumEquals( - roundAfter.answeredInRound, - roundAfterLatest.answeredInRound, - ) - }) - }) - - describe('when an oracle submits for a round twice', () => { - it('reverts', async () => { - await aggregator.connect(personas.Neil).submit(nextRound, answer) - - await evmRevert( - aggregator.connect(personas.Neil).submit(nextRound, answer), - 'cannot report on previous rounds', - ) - }) - }) - - describe('when updated after the max answers submitted', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator) - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - - it('reverts', async () => { - await evmRevert( - aggregator.connect(personas.Ned).submit(nextRound, answer), - 'round not accepting submissions', - ) - }) - }) - - describe('when a new highest round number is passed in', () => { - it('increments the answer round', async () => { - const startingState = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - bigNumEquals(1, startingState._roundId) - - await advanceRound(aggregator, oracles) - - const updatedState = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - bigNumEquals(2, updatedState._roundId) - }) - - it('sets the startedAt time for the reporting round', async () => { - evmRevert(aggregator.getRoundData(nextRound), 'No data present') - - const tx = await aggregator - .connect(oracles[0]) - .submit(nextRound, answer) - await aggregator.connect(oracles[1]).submit(nextRound, answer) - await aggregator.connect(oracles[2]).submit(nextRound, answer) - const receipt = await tx.wait() - const block = await ethers.provider.getBlock(receipt.blockHash ?? '') - - const round = await aggregator.getRoundData(nextRound) - bigNumEquals(BigNumber.from(block.timestamp), round.startedAt) - }) - - it('announces a new round by emitting a log', async () => { - const tx = await aggregator - .connect(personas.Neil) - .submit(nextRound, answer) - const receipt = await tx.wait() - - const topics = receipt.logs?.[0].topics ?? [] - const roundNumber = BigNumber.from(topics[1]) - const startedBy = evmWordToAddress(topics[2]) - - bigNumEquals(nextRound, roundNumber.toNumber()) - bigNumEquals(startedBy, await personas.Neil.getAddress()) - }) - }) - - describe('when a round is passed in higher than expected', () => { - it('reverts', async () => { - await evmRevert( - aggregator.connect(personas.Neil).submit(nextRound + 1, answer), - 'invalid round to report', - ) - }) - }) - - describe('when called by a non-oracle', () => { - it('reverts', async () => { - await evmRevert( - aggregator.connect(personas.Carol).submit(nextRound, answer), - 'not enabled oracle', - ) - }) - }) - - describe('when there are not sufficient available funds', () => { - beforeEach(async () => { - await aggregator - .connect(personas.Carol) - .withdrawFunds( - await personas.Carol.getAddress(), - deposit.sub(paymentAmount.mul(oracles.length).mul(reserveRounds)), - ) - - // drain remaining funds - await advanceRound(aggregator, oracles) - await advanceRound(aggregator, oracles) - }) - - it('reverts', async () => { - await evmRevert( - aggregator.connect(personas.Neil).submit(nextRound, answer), - 'SafeMath: subtraction overflow', - ) - }) - }) - - describe('when a new round opens before the previous rounds closes', () => { - beforeEach(async () => { - oracles = [personas.Nancy, personas.Norbert] - await addOracles(aggregator, oracles, 3, 4, rrDelay) - await advanceRound(aggregator, [ - personas.Nelly, - personas.Neil, - personas.Nancy, - ]) - - // start the next round - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - }) - - it('still allows the previous round to be answered', async () => { - await aggregator.connect(personas.Ned).submit(nextRound - 1, answer) - }) - - describe('once the current round is answered', () => { - beforeEach(async () => { - oracles = [personas.Neil, personas.Nancy] - for (let i = 0; i < oracles.length; i++) { - await aggregator.connect(oracles[i]).submit(nextRound, answer) - } - }) - - it('does not allow reports for the previous round', async () => { - await evmRevert( - aggregator.connect(personas.Ned).submit(nextRound - 1, answer), - 'invalid round to report', - ) - }) - }) - - describe('when the previous round has finished', () => { - beforeEach(async () => { - await aggregator - .connect(personas.Norbert) - .submit(nextRound - 1, answer) - }) - - it('does not allow reports for the previous round', async () => { - await evmRevert( - aggregator.connect(personas.Ned).submit(nextRound - 1, answer), - 'round not accepting submissions', - ) - }) - }) - }) - - describe('when price is updated mid-round', () => { - const newAmount = toWei('50') - - it('pays the same amount to all oracles per round', async () => { - await link.transfer( - aggregator.address, - newAmount.mul(oracles.length).mul(reserveRounds), - ) - await aggregator.updateAvailableFunds() - - bigNumEquals( - 0, - await aggregator - .connect(personas.Neil) - .withdrawablePayment(await personas.Neil.getAddress()), - ) - bigNumEquals( - 0, - await aggregator - .connect(personas.Nelly) - .withdrawablePayment(await personas.Nelly.getAddress()), - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - - await updateFutureRounds(aggregator, { payment: newAmount }) - - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - - bigNumEquals( - paymentAmount, - await aggregator - .connect(personas.Neil) - .withdrawablePayment(await personas.Neil.getAddress()), - ) - bigNumEquals( - paymentAmount, - await aggregator - .connect(personas.Nelly) - .withdrawablePayment(await personas.Nelly.getAddress()), - ) - }) - }) - - describe('when delay is on', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { - minAnswers: oracles.length, - maxAnswers: oracles.length, - restartDelay: 1, - }) - }) - - it("does not revert on the oracle's first round", async () => { - // Since lastUpdatedRound defaults to zero and that's the only - // indication that an oracle hasn't responded, this test guards against - // the situation where we don't check that and no one can start a round. - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - - it('does revert before the delay', async () => { - await aggregator.connect(personas.Neil).submit(nextRound, answer) - - nextRound++ - - await evmRevert( - aggregator.connect(personas.Neil).submit(nextRound, answer), - 'previous round not supersedable', - ) - }) - }) - - describe('when an oracle starts a round before the restart delay is over', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator.connect(personas.Carol), { - minAnswers: 1, - maxAnswers: 1, - }) - - oracles = [personas.Neil, personas.Ned, personas.Nelly] - for (let i = 0; i < oracles.length; i++) { - await aggregator.connect(oracles[i]).submit(nextRound, answer) - nextRound++ - } - - const newDelay = 2 - // Since Ned and Nelly have answered recently, and we set the delay - // to 2, only Nelly can answer as she is the only oracle that hasn't - // started the last two rounds. - await updateFutureRounds(aggregator, { - maxAnswers: oracles.length, - restartDelay: newDelay, - }) - }) - - describe('when called by an oracle who has not answered recently', () => { - it('does not revert', async () => { - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - }) - - describe('when called by an oracle who answered recently', () => { - it('reverts', async () => { - await evmRevert( - aggregator.connect(personas.Ned).submit(nextRound, answer), - 'round not accepting submissions', - ) - - await evmRevert( - aggregator.connect(personas.Nelly).submit(nextRound, answer), - 'round not accepting submissions', - ) - }) - }) - }) - - describe('when the price is not updated for a round', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { - minAnswers: oracles.length, - maxAnswers: oracles.length, - restartDelay: 1, - }) - - for (const oracle of oracles) { - await aggregator.connect(oracle).submit(nextRound, answer) - } - nextRound++ - - await aggregator.connect(personas.Ned).submit(nextRound, answer) - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - - await increaseTimeBy(timeout + 1, ethers.provider) - nextRound++ - }) - - it('allows a new round to be started', async () => { - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - }) - - it('sets the info for the previous round', async () => { - const previousRound = nextRound - 1 - let updated = await aggregator.getTimestamp(previousRound) - let ans = await aggregator.getAnswer(previousRound) - assert.equal(0, updated.toNumber()) - assert.equal(0, ans.toNumber()) - - const tx = await aggregator - .connect(personas.Nelly) - .submit(nextRound, answer) - const receipt = await tx.wait() - - const block = await ethers.provider.getBlock(receipt.blockHash ?? '') - - updated = await aggregator.getTimestamp(previousRound) - ans = await aggregator.getAnswer(previousRound) - bigNumEquals(BigNumber.from(block.timestamp), updated) - assert.equal(answer, ans.toNumber()) - - const round = await aggregator.getRoundData(previousRound) - bigNumEquals(previousRound, round.roundId) - bigNumEquals(ans, round.answer) - bigNumEquals(updated, round.updatedAt) - bigNumEquals(previousRound - 1, round.answeredInRound) - }) - - it('sets the previous round as timed out', async () => { - const previousRound = nextRound - 1 - evmRevert(aggregator.getRoundData(previousRound), 'No data present') - - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - - const round = await aggregator.getRoundData(previousRound) - assert.notEqual(round.roundId, round.answeredInRound) - bigNumEquals(previousRound - 1, round.answeredInRound) - }) - - it('still respects the delay restriction', async () => { - // expected to revert because the sender started the last round - await evmRevert( - aggregator.connect(personas.Ned).submit(nextRound, answer), - ) - }) - - it('uses the timeout set at the beginning of the round', async () => { - await updateFutureRounds(aggregator, { - timeout: timeout + 100000, - }) - - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - }) - }) - - describe('submitting values near the edges of allowed values', () => { - it('rejects values below the submission value range', async () => { - await evmRevert( - aggregator - .connect(personas.Neil) - .submit(nextRound, minSubmissionValue.sub(1)), - 'value below minSubmissionValue', - ) - }) - - it('accepts submissions equal to the min submission value', async () => { - await aggregator - .connect(personas.Neil) - .submit(nextRound, minSubmissionValue) - }) - - it('accepts submissions equal to the max submission value', async () => { - await aggregator - .connect(personas.Neil) - .submit(nextRound, maxSubmissionValue) - }) - - it('rejects submissions equal to the max submission value', async () => { - await evmRevert( - aggregator - .connect(personas.Neil) - .submit(nextRound, maxSubmissionValue.add(1)), - 'value above maxSubmissionValue', - ) - }) - }) - - describe('when a validator is set', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { minAnswers: 1, maxAnswers: 1 }) - oracles = [personas.Nelly] - - validator = await validatorMockFactory.connect(personas.Carol).deploy() - await aggregator.connect(personas.Carol).setValidator(validator.address) - }) - - it('calls out to the validator', async () => { - await expect( - aggregator.connect(personas.Nelly).submit(nextRound, answer), - ) - .to.emit(validator, 'Validated') - .withArgs(0, 0, nextRound, answer) - }) - }) - - describe('when the answer validator eats all gas', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { minAnswers: 1, maxAnswers: 1 }) - oracles = [personas.Nelly] - - gasGuzzler = await gasGuzzlerFactory.connect(personas.Carol).deploy() - await aggregator - .connect(personas.Carol) - .setValidator(gasGuzzler.address) - assert.equal(gasGuzzler.address, await aggregator.validator()) - }) - - it('still updates', async () => { - bigNumEquals(0, await aggregator.latestAnswer()) - - await aggregator - .connect(personas.Nelly) - .submit(nextRound, answer, { gasLimit: 500000 }) - - bigNumEquals(answer, await aggregator.latestAnswer()) - }) - }) - }) - - describe('#getAnswer', () => { - const answers = [1, 10, 101, 1010, 10101, 101010, 1010101] - - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - - for (const answer of answers) { - await aggregator.connect(personas.Neil).submit(nextRound++, answer) - } - }) - - it('retrieves the answer recorded for past rounds', async () => { - for (let i = nextRound; i < nextRound; i++) { - const answer = await aggregator.getAnswer(i) - bigNumEquals(BigNumber.from(answers[i - 1]), answer) - } - }) - - it("returns 0 for answers greater than uint32's max", async () => { - const overflowedId = BigNumber.from(2).pow(32).add(1) - const answer = await aggregator.getAnswer(overflowedId) - bigNumEquals(0, answer) - }) - }) - - describe('#getTimestamp', () => { - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - - for (let i = 0; i < 10; i++) { - await aggregator.connect(personas.Neil).submit(nextRound++, i + 1) - } - }) - - it('retrieves the answer recorded for past rounds', async () => { - let lastTimestamp = ethers.constants.Zero - - for (let i = 1; i < nextRound; i++) { - const currentTimestamp = await aggregator.getTimestamp(i) - assert.isAtLeast(currentTimestamp.toNumber(), lastTimestamp.toNumber()) - lastTimestamp = currentTimestamp - } - }) - - it("returns 0 for answers greater than uint32's max", async () => { - const overflowedId = BigNumber.from(2).pow(32).add(1) - const answer = await aggregator.getTimestamp(overflowedId) - bigNumEquals(0, answer) - }) - }) - - describe('#changeOracles', () => { - describe('adding oracles', () => { - it('increases the oracle count', async () => { - const pastCount = await aggregator.oracleCount() - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - const currentCount = await aggregator.oracleCount() - - bigNumEquals(currentCount, pastCount + 1) - }) - - it('adds the address in getOracles', async () => { - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - assert.deepEqual( - [await personas.Neil.getAddress()], - await aggregator.getOracles(), - ) - }) - - it('updates the round details', async () => { - await addOracles( - aggregator, - [personas.Neil, personas.Ned, personas.Nelly], - 1, - 3, - 2, - ) - bigNumEquals(1, await aggregator.minSubmissionCount()) - bigNumEquals(3, await aggregator.maxSubmissionCount()) - bigNumEquals(2, await aggregator.restartDelay()) - }) - - it('emits a log', async () => { - const tx = await aggregator - .connect(personas.Carol) - .changeOracles( - [], - [await personas.Ned.getAddress()], - [await personas.Neil.getAddress()], - 1, - 1, - 0, - ) - expect(tx) - .to.emit(aggregator, 'OraclePermissionsUpdated') - .withArgs(await personas.Ned.getAddress(), true) - - expect(tx) - .to.emit(aggregator, 'OracleAdminUpdated') - .withArgs( - await personas.Ned.getAddress(), - await personas.Neil.getAddress(), - ) - }) - - describe('when the oracle has already been added', () => { - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - }) - - it('reverts', async () => { - await evmRevert( - addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay), - 'oracle already enabled', - ) - }) - }) - - describe('when called by anyone but the owner', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Neil) - .changeOracles( - [], - [await personas.Neil.getAddress()], - [await personas.Neil.getAddress()], - minAns, - maxAns, - rrDelay, - ), - 'Only callable by owner', - ) - }) - }) - - describe('when an oracle gets added mid-round', () => { - beforeEach(async () => { - oracles = [personas.Neil, personas.Ned] - await addOracles( - aggregator, - oracles, - oracles.length, - oracles.length, - rrDelay, - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - - await addOracles( - aggregator, - [personas.Nelly], - oracles.length + 1, - oracles.length + 1, - rrDelay, - ) - }) - - it('does not allow the oracle to update the round', async () => { - await evmRevert( - aggregator.connect(personas.Nelly).submit(nextRound, answer), - 'not yet enabled oracle', - ) - }) - - it('does allow the oracle to update future rounds', async () => { - // complete round - await aggregator.connect(personas.Ned).submit(nextRound, answer) - - // now can participate in new rounds - await aggregator.connect(personas.Nelly).submit(nextRound + 1, answer) - }) - }) - - describe('when an oracle is added after removed for a round', () => { - it('allows the oracle to update', async () => { - oracles = [personas.Neil, personas.Nelly] - await addOracles( - aggregator, - oracles, - oracles.length, - oracles.length, - rrDelay, - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - nextRound++ - - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Nelly.getAddress()], - [], - [], - 1, - 1, - rrDelay, - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - nextRound++ - - await addOracles(aggregator, [personas.Nelly], 1, 1, rrDelay) - - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - }) - }) - - describe('when an oracle is added and immediately removed mid-round', () => { - it('allows the oracle to update', async () => { - await addOracles( - aggregator, - oracles, - oracles.length, - oracles.length, - rrDelay, - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - nextRound++ - - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Nelly.getAddress()], - [], - [], - 1, - 1, - rrDelay, - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - nextRound++ - - await addOracles(aggregator, [personas.Nelly], 1, 1, rrDelay) - - await aggregator.connect(personas.Nelly).submit(nextRound, answer) - }) - }) - - describe('when an oracle is re-added with a different admin address', () => { - it('reverts', async () => { - await addOracles( - aggregator, - oracles, - oracles.length, - oracles.length, - rrDelay, - ) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Nelly.getAddress()], - [], - [], - 1, - 1, - rrDelay, - ) - - await evmRevert( - aggregator - .connect(personas.Carol) - .changeOracles( - [], - [await personas.Nelly.getAddress()], - [await personas.Carol.getAddress()], - 1, - 1, - rrDelay, - ), - 'owner cannot overwrite admin', - ) - }) - }) - - const limit = 77 - describe(`when adding more than ${limit} oracles`, () => { - let oracles: Signer[] - - beforeEach(async () => { - oracles = [] - for (let i = 0; i < limit; i++) { - const account = await new ethers.Wallet( - randomBytes(32), - ethers.provider, - ) - await personas.Default.sendTransaction({ - to: account.address, - value: toWei('0.1'), - }) - - oracles.push(account) - } - - await link.transfer( - aggregator.address, - paymentAmount.mul(limit).mul(reserveRounds), - ) - await aggregator.updateAvailableFunds() - - let addresses = oracles.slice(0, 50).map(async (o) => o.getAddress()) - await aggregator - .connect(personas.Carol) - .changeOracles([], addresses, addresses, 1, 50, rrDelay) - // add in two transactions to avoid gas limit issues - addresses = oracles.slice(50, 100).map(async (o) => o.getAddress()) - await aggregator - .connect(personas.Carol) - .changeOracles([], addresses, addresses, 1, oracles.length, rrDelay) - }) - - it('not use too much gas [ @skip-coverage ]', async () => { - let tx: any - assert.deepEqual( - // test adveserial quickselect algo - [2, 4, 6, 8, 10, 12, 14, 16, 1, 9, 5, 11, 3, 13, 7, 15], - adverserialQuickselectList(16), - ) - const inputs = adverserialQuickselectList(limit) - for (let i = 0; i < limit; i++) { - tx = await aggregator - .connect(oracles[i]) - .submit(nextRound, inputs[i]) - } - assert.isTrue(!!tx) - if (tx) { - const receipt = await tx.wait() - assert.isBelow(receipt.gasUsed.toNumber(), 600_000) - } - }) - - function adverserialQuickselectList(len: number): number[] { - const xs: number[] = [] - const pi: number[] = [] - for (let i = 0; i < len; i++) { - pi[i] = i - xs[i] = 0 - } - - for (let l = len; l > 0; l--) { - const pivot = Math.floor((l - 1) / 2) - xs[pi[pivot]] = l - const temp = pi[l - 1] - pi[l - 1] = pi[pivot] - pi[pivot] = temp - } - return xs - } - - it('reverts when another oracle is added', async () => { - await evmRevert( - aggregator - .connect(personas.Carol) - .changeOracles( - [], - [await personas.Neil.getAddress()], - [await personas.Neil.getAddress()], - limit + 1, - limit + 1, - rrDelay, - ), - 'max oracles allowed', - ) - }) - }) - - it('reverts when minSubmissions is set to 0', async () => { - await evmRevert( - addOracles(aggregator, [personas.Neil], 0, 0, 0), - 'min must be greater than 0', - ) - }) - }) - - describe('removing oracles', () => { - beforeEach(async () => { - oracles = [personas.Neil, personas.Nelly] - await addOracles( - aggregator, - oracles, - oracles.length, - oracles.length, - rrDelay, - ) - }) - - it('decreases the oracle count', async () => { - const pastCount = await aggregator.oracleCount() - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ) - const currentCount = await aggregator.oracleCount() - - expect(currentCount).to.equal(pastCount - 1) - }) - - it('updates the round details', async () => { - await aggregator - .connect(personas.Carol) - .changeOracles([await personas.Neil.getAddress()], [], [], 1, 1, 0) - - bigNumEquals(1, await aggregator.minSubmissionCount()) - bigNumEquals(1, await aggregator.maxSubmissionCount()) - bigNumEquals(ethers.constants.Zero, await aggregator.restartDelay()) - }) - - it('emits a log', async () => { - await expect( - aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ), - ) - .to.emit(aggregator, 'OraclePermissionsUpdated') - .withArgs(await personas.Neil.getAddress(), false) - }) - - it('removes the address in getOracles', async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ) - assert.deepEqual( - [await personas.Nelly.getAddress()], - await aggregator.getOracles(), - ) - }) - - describe('when the oracle is not currently added', () => { - beforeEach(async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ) - }) - - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ), - 'oracle not enabled', - ) - }) - }) - - describe('when removing the last oracle', () => { - it('does not revert', async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ) - - await aggregator - .connect(personas.Carol) - .changeOracles([await personas.Nelly.getAddress()], [], [], 0, 0, 0) - }) - }) - - describe('when called by anyone but the owner', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Ned) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - 0, - 0, - rrDelay, - ), - 'Only callable by owner', - ) - }) - }) - - describe('when an oracle gets removed', () => { - beforeEach(async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Nelly.getAddress()], - [], - [], - 1, - 1, - rrDelay, - ) - }) - - it('is allowed to report on one more round', async () => { - // next round - await advanceRound(aggregator, [personas.Nelly]) - // finish round - await advanceRound(aggregator, [personas.Neil]) - - // cannot participate in future rounds - await evmRevert( - aggregator.connect(personas.Nelly).submit(nextRound, answer), - 'no longer allowed oracle', - ) - }) - }) - - describe('when an oracle gets removed mid-round', () => { - beforeEach(async () => { - await aggregator.connect(personas.Neil).submit(nextRound, answer) - - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Nelly.getAddress()], - [], - [], - 1, - 1, - rrDelay, - ) - }) - - it('is allowed to finish that round and one more round', async () => { - await advanceRound(aggregator, [personas.Nelly]) // finish round - - await advanceRound(aggregator, [personas.Nelly]) // next round - - // cannot participate in future rounds - await evmRevert( - aggregator.connect(personas.Nelly).submit(nextRound, answer), - 'no longer allowed oracle', - ) - }) - }) - - it('reverts when minSubmissions is set to 0', async () => { - await evmRevert( - aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Nelly.getAddress()], - [], - [], - 0, - 0, - 0, - ), - 'min must be greater than 0', - ) - }) - }) - - describe('adding and removing oracles at once', () => { - beforeEach(async () => { - oracles = [personas.Neil, personas.Ned] - await addOracles(aggregator, oracles, 1, 1, rrDelay) - }) - - it('can swap out oracles', async () => { - assert.include( - await aggregator.getOracles(), - await personas.Ned.getAddress(), - ) - assert.notInclude( - await aggregator.getOracles(), - await personas.Nelly.getAddress(), - ) - - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Ned.getAddress()], - [await personas.Nelly.getAddress()], - [await personas.Nelly.getAddress()], - 1, - 1, - rrDelay, - ) - - assert.notInclude( - await aggregator.getOracles(), - await personas.Ned.getAddress(), - ) - assert.include( - await aggregator.getOracles(), - await personas.Nelly.getAddress(), - ) - }) - - it('is possible to remove and add the same address', async () => { - assert.include( - await aggregator.getOracles(), - await personas.Ned.getAddress(), - ) - - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Ned.getAddress()], - [await personas.Ned.getAddress()], - [await personas.Ned.getAddress()], - 1, - 1, - rrDelay, - ) - - assert.include( - await aggregator.getOracles(), - await personas.Ned.getAddress(), - ) - }) - }) - }) - - describe('#getOracles', () => { - describe('after adding oracles', () => { - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - - assert.deepEqual( - [await personas.Neil.getAddress()], - await aggregator.getOracles(), - ) - }) - - it('returns the addresses of added oracles', async () => { - await addOracles(aggregator, [personas.Ned], minAns, maxAns, rrDelay) - - assert.deepEqual( - [await personas.Neil.getAddress(), await personas.Ned.getAddress()], - await aggregator.getOracles(), - ) - - await addOracles(aggregator, [personas.Nelly], minAns, maxAns, rrDelay) - assert.deepEqual( - [ - await personas.Neil.getAddress(), - await personas.Ned.getAddress(), - await personas.Nelly.getAddress(), - ], - await aggregator.getOracles(), - ) - }) - }) - - describe('after removing oracles', () => { - beforeEach(async () => { - await addOracles( - aggregator, - [personas.Neil, personas.Ned, personas.Nelly], - minAns, - maxAns, - rrDelay, - ) - - assert.deepEqual( - [ - await personas.Neil.getAddress(), - await personas.Ned.getAddress(), - await personas.Nelly.getAddress(), - ], - await aggregator.getOracles(), - ) - }) - - it('reorders when removing from the beginning', async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Neil.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ) - assert.deepEqual( - [await personas.Nelly.getAddress(), await personas.Ned.getAddress()], - await aggregator.getOracles(), - ) - }) - - it('reorders when removing from the middle', async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Ned.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ) - assert.deepEqual( - [await personas.Neil.getAddress(), await personas.Nelly.getAddress()], - await aggregator.getOracles(), - ) - }) - - it('pops the last node off at the end', async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [await personas.Nelly.getAddress()], - [], - [], - minAns, - maxAns, - rrDelay, - ) - assert.deepEqual( - [await personas.Neil.getAddress(), await personas.Ned.getAddress()], - await aggregator.getOracles(), - ) - }) - }) - }) - - describe('#withdrawFunds', () => { - it('succeeds', async () => { - await aggregator - .connect(personas.Carol) - .withdrawFunds(await personas.Carol.getAddress(), deposit) - - bigNumEquals(0, await aggregator.availableFunds()) - bigNumEquals( - deposit, - await link.balanceOf(await personas.Carol.getAddress()), - ) - }) - - it('does not let withdrawals happen multiple times', async () => { - await aggregator - .connect(personas.Carol) - .withdrawFunds(await personas.Carol.getAddress(), deposit) - - await evmRevert( - aggregator - .connect(personas.Carol) - .withdrawFunds(await personas.Carol.getAddress(), deposit), - 'insufficient reserve funds', - ) - }) - - describe('with a number higher than the available LINK balance', () => { - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - - it('fails', async () => { - await evmRevert( - aggregator - .connect(personas.Carol) - .withdrawFunds(await personas.Carol.getAddress(), deposit), - 'insufficient reserve funds', - ) - - bigNumEquals( - deposit.sub(paymentAmount), - await aggregator.availableFunds(), - ) - }) - }) - - describe('with oracles still present', () => { - beforeEach(async () => { - oracles = [personas.Neil, personas.Ned, personas.Nelly] - await addOracles(aggregator, oracles, 1, 1, rrDelay) - - bigNumEquals(deposit, await aggregator.availableFunds()) - }) - - it('does not allow withdrawal with less than 2x rounds of payments', async () => { - const oracleReserve = paymentAmount - .mul(oracles.length) - .mul(reserveRounds) - const allowed = deposit.sub(oracleReserve) - - //one more than the allowed amount cannot be withdrawn - await evmRevert( - aggregator - .connect(personas.Carol) - .withdrawFunds(await personas.Carol.getAddress(), allowed.add(1)), - 'insufficient reserve funds', - ) - - // the allowed amount can be withdrawn - await aggregator - .connect(personas.Carol) - .withdrawFunds(await personas.Carol.getAddress(), allowed) - }) - }) - - describe('when called by a non-owner', () => { - it('fails', async () => { - await evmRevert( - aggregator - .connect(personas.Eddy) - .withdrawFunds(await personas.Carol.getAddress(), deposit), - 'Only callable by owner', - ) - - bigNumEquals(deposit, await aggregator.availableFunds()) - }) - }) - }) - - describe('#updateFutureRounds', () => { - let minSubmissionCount, maxSubmissionCount - const newPaymentAmount = toWei('2') - const newMin = 1 - const newMax = 3 - const newDelay = 2 - - beforeEach(async () => { - oracles = [personas.Neil, personas.Ned, personas.Nelly] - minSubmissionCount = oracles.length - maxSubmissionCount = oracles.length - await addOracles( - aggregator, - oracles, - minSubmissionCount, - maxSubmissionCount, - rrDelay, - ) - - bigNumEquals(paymentAmount, await aggregator.paymentAmount()) - assert.equal(minSubmissionCount, await aggregator.minSubmissionCount()) - assert.equal(maxSubmissionCount, await aggregator.maxSubmissionCount()) - }) - - it('updates the min and max answer counts', async () => { - await updateFutureRounds(aggregator, { - payment: newPaymentAmount, - minAnswers: newMin, - maxAnswers: newMax, - restartDelay: newDelay, - }) - - bigNumEquals(newPaymentAmount, await aggregator.paymentAmount()) - bigNumEquals( - BigNumber.from(newMin), - await aggregator.minSubmissionCount(), - ) - bigNumEquals( - BigNumber.from(newMax), - await aggregator.maxSubmissionCount(), - ) - bigNumEquals(BigNumber.from(newDelay), await aggregator.restartDelay()) - }) - - it('emits a log announcing the new round details', async () => { - await expect( - updateFutureRounds(aggregator, { - payment: newPaymentAmount, - minAnswers: newMin, - maxAnswers: newMax, - restartDelay: newDelay, - timeout: timeout + 1, - }), - ) - .to.emit(aggregator, 'RoundDetailsUpdated') - .withArgs(newPaymentAmount, newMin, newMax, newDelay, timeout + 1) - }) - - describe('when it is set to higher than the number or oracles', () => { - it('reverts', async () => { - await evmRevert( - updateFutureRounds(aggregator, { - maxAnswers: 4, - }), - 'max cannot exceed total', - ) - }) - }) - - describe('when it sets the min higher than the max', () => { - it('reverts', async () => { - await evmRevert( - updateFutureRounds(aggregator, { - minAnswers: 3, - maxAnswers: 2, - }), - 'max must equal/exceed min', - ) - }) - }) - - describe('when delay equal or greater the oracle count', () => { - it('reverts', async () => { - await evmRevert( - updateFutureRounds(aggregator, { - restartDelay: 3, - }), - 'delay cannot exceed total', - ) - }) - }) - - describe('when the payment amount does not cover reserve rounds', () => { - beforeEach(async () => {}) - - it('reverts', async () => { - const most = deposit.div(oracles.length * reserveRounds) - - // Relaxed check for the revert message due to a bug in ethers where any error message - // that starts with insufficient funds will be incorrectly returned as 'insufficient funds for intrinsic transaction cost' - await updateFutureRounds(aggregator, { - payment: most.add(1), - }).then( - () => { - // onFulfillment callback - fail('expected to revert but did not') - }, - (error: any) => { - // onRejected callback - const message = - error instanceof Object && 'message' in error - ? error.message - : JSON.stringify(error) - assert.isTrue(message.includes('insufficient funds')) - }, - ) - - await updateFutureRounds(aggregator, { - payment: most, - }) - }) - }) - - describe('min oracles is set to 0', () => { - it('reverts', async () => { - await evmRevert( - aggregator.updateFutureRounds(paymentAmount, 0, 0, rrDelay, timeout), - 'min must be greater than 0', - ) - }) - }) - - describe('when called by anyone but the owner', () => { - it('reverts', async () => { - await evmRevert( - updateFutureRounds(aggregator.connect(personas.Ned)), - 'Only callable by owner', - ) - }) - }) - }) - - describe('#updateAvailableFunds', () => { - it('checks the LINK token to see if any additional funds are available', async () => { - const originalBalance = await aggregator.availableFunds() - - await aggregator.updateAvailableFunds() - - bigNumEquals(originalBalance, await aggregator.availableFunds()) - - await link.transfer(aggregator.address, deposit) - await aggregator.updateAvailableFunds() - - const newBalance = await aggregator.availableFunds() - bigNumEquals(originalBalance.add(deposit), newBalance) - }) - - it('removes allocated funds from the available balance', async () => { - const originalBalance = await aggregator.availableFunds() - - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - await aggregator.connect(personas.Neil).submit(nextRound, answer) - await link.transfer(aggregator.address, deposit) - await aggregator.updateAvailableFunds() - - const expected = originalBalance.add(deposit).sub(paymentAmount) - const newBalance = await aggregator.availableFunds() - bigNumEquals(expected, newBalance) - }) - - it('emits a log', async () => { - await link.transfer(aggregator.address, deposit) - - const tx = await aggregator.updateAvailableFunds() - const receipt = await tx.wait() - - const reportedBalance = BigNumber.from(receipt.logs?.[0].topics[1] ?? -1) - bigNumEquals(await aggregator.availableFunds(), reportedBalance) - }) - - describe('when the available funds have not changed', () => { - it('does not emit a log', async () => { - const tx = await aggregator.updateAvailableFunds() - const receipt = await tx.wait() - - assert.equal(0, receipt.logs?.length) - }) - }) - }) - - describe('#withdrawPayment', () => { - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], minAns, maxAns, rrDelay) - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - - it('transfers LINK to the recipient', async () => { - const originalBalance = await link.balanceOf(aggregator.address) - bigNumEquals(0, await link.balanceOf(await personas.Neil.getAddress())) - - await aggregator - .connect(personas.Neil) - .withdrawPayment( - await personas.Neil.getAddress(), - await personas.Neil.getAddress(), - paymentAmount, - ) - - bigNumEquals( - originalBalance.sub(paymentAmount), - await link.balanceOf(aggregator.address), - ) - bigNumEquals( - paymentAmount, - await link.balanceOf(await personas.Neil.getAddress()), - ) - }) - - it('decrements the allocated funds counter', async () => { - const originalAllocation = await aggregator.allocatedFunds() - - await aggregator - .connect(personas.Neil) - .withdrawPayment( - await personas.Neil.getAddress(), - await personas.Neil.getAddress(), - paymentAmount, - ) - - bigNumEquals( - originalAllocation.sub(paymentAmount), - await aggregator.allocatedFunds(), - ) - }) - - describe('when the caller withdraws more than they have', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Neil) - .withdrawPayment( - await personas.Neil.getAddress(), - await personas.Neil.getAddress(), - paymentAmount.add(BigNumber.from(1)), - ), - 'insufficient withdrawable funds', - ) - }) - }) - - describe('when the caller is not the admin', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Nelly) - .withdrawPayment( - await personas.Neil.getAddress(), - await personas.Nelly.getAddress(), - BigNumber.from(1), - ), - 'only callable by admin', - ) - }) - }) - }) - - describe('#transferAdmin', () => { - beforeEach(async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [], - [await personas.Ned.getAddress()], - [await personas.Neil.getAddress()], - minAns, - maxAns, - rrDelay, - ) - }) - - describe('when the admin tries to transfer the admin', () => { - it('works', async () => { - await expect( - aggregator - .connect(personas.Neil) - .transferAdmin( - await personas.Ned.getAddress(), - await personas.Nelly.getAddress(), - ), - ) - .to.emit(aggregator, 'OracleAdminUpdateRequested') - .withArgs( - await personas.Ned.getAddress(), - await personas.Neil.getAddress(), - await personas.Nelly.getAddress(), - ) - assert.equal( - await personas.Neil.getAddress(), - await aggregator.getAdmin(await personas.Ned.getAddress()), - ) - }) - }) - - describe('when the non-admin owner tries to update the admin', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Carol) - .transferAdmin( - await personas.Ned.getAddress(), - await personas.Nelly.getAddress(), - ), - 'only callable by admin', - ) - }) - }) - - describe('when the non-admin oracle tries to update the admin', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Ned) - .transferAdmin( - await personas.Ned.getAddress(), - await personas.Nelly.getAddress(), - ), - 'only callable by admin', - ) - }) - }) - }) - - describe('#acceptAdmin', () => { - beforeEach(async () => { - await aggregator - .connect(personas.Carol) - .changeOracles( - [], - [await personas.Ned.getAddress()], - [await personas.Neil.getAddress()], - minAns, - maxAns, - rrDelay, - ) - const tx = await aggregator - .connect(personas.Neil) - .transferAdmin( - await personas.Ned.getAddress(), - await personas.Nelly.getAddress(), - ) - await tx.wait() - }) - - describe('when the new admin tries to accept', () => { - it('works', async () => { - await expect( - aggregator - .connect(personas.Nelly) - .acceptAdmin(await personas.Ned.getAddress()), - ) - .to.emit(aggregator, 'OracleAdminUpdated') - .withArgs( - await personas.Ned.getAddress(), - await personas.Nelly.getAddress(), - ) - assert.equal( - await personas.Nelly.getAddress(), - await aggregator.getAdmin(await personas.Ned.getAddress()), - ) - }) - }) - - describe('when someone other than the new admin tries to accept', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Ned) - .acceptAdmin(await personas.Ned.getAddress()), - 'only callable by pending admin', - ) - await evmRevert( - aggregator - .connect(personas.Neil) - .acceptAdmin(await personas.Ned.getAddress()), - 'only callable by pending admin', - ) - }) - }) - }) - - describe('#onTokenTransfer', () => { - it('updates the available balance', async () => { - const originalBalance = await aggregator.availableFunds() - - await aggregator.updateAvailableFunds() - - bigNumEquals(originalBalance, await aggregator.availableFunds()) - - await link.transferAndCall(aggregator.address, deposit, '0x') - - const newBalance = await aggregator.availableFunds() - bigNumEquals(originalBalance.add(deposit), newBalance) - }) - - it('reverts given calldata', async () => { - await evmRevert( - // error message is not bubbled up by link token - link.transferAndCall(aggregator.address, deposit, '0x12345678'), - ) - }) - }) - - describe('#requestNewRound', () => { - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], 1, 1, 0) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - nextRound = nextRound + 1 - - await aggregator.setRequesterPermissions( - await personas.Carol.getAddress(), - true, - 0, - ) - }) - - it('announces a new round via log event', async () => { - await expect(aggregator.requestNewRound()).to.emit(aggregator, 'NewRound') - }) - - it('returns the new round ID', async () => { - testHelper = await testHelperFactory.connect(personas.Carol).deploy() - await aggregator.setRequesterPermissions(testHelper.address, true, 0) - let roundId = await testHelper.requestedRoundId() - assert.equal(roundId.toNumber(), 0) - - await testHelper.requestNewRound(aggregator.address) - - // return value captured by test helper - roundId = await testHelper.requestedRoundId() - assert.isAbove(roundId.toNumber(), 0) - }) - - describe('when there is a round in progress', () => { - beforeEach(async () => { - await aggregator.requestNewRound() - }) - - it('reverts', async () => { - await evmRevert( - aggregator.requestNewRound(), - 'prev round must be supersedable', - ) - }) - - describe('when that round has timed out', () => { - beforeEach(async () => { - await increaseTimeBy(timeout + 1, ethers.provider) - await mineBlock(ethers.provider) - }) - - it('starts a new round', async () => { - await expect(aggregator.requestNewRound()).to.emit( - aggregator, - 'NewRound', - ) - }) - }) - }) - - describe('when there is a restart delay set', () => { - beforeEach(async () => { - await aggregator.setRequesterPermissions( - await personas.Eddy.getAddress(), - true, - 1, - ) - }) - - it('reverts if a round is started before the delay', async () => { - await aggregator.connect(personas.Eddy).requestNewRound() - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - nextRound = nextRound + 1 - - // Eddy can't start because of the delay - await evmRevert( - aggregator.connect(personas.Eddy).requestNewRound(), - 'must delay requests', - ) - // Carol starts a new round instead - await aggregator.connect(personas.Carol).requestNewRound() - - // round completes - await aggregator.connect(personas.Neil).submit(nextRound, answer) - nextRound = nextRound + 1 - - // now Eddy can start again - await aggregator.connect(personas.Eddy).requestNewRound() - }) - }) - - describe('when all oracles have been removed and then re-added', () => { - it('does not get stuck', async () => { - await aggregator - .connect(personas.Carol) - .changeOracles([await personas.Neil.getAddress()], [], [], 0, 0, 0) - - // advance a few rounds - for (let i = 0; i < 7; i++) { - await aggregator.requestNewRound() - nextRound = nextRound + 1 - await increaseTimeBy(timeout + 1, ethers.provider) - await mineBlock(ethers.provider) - } - - await addOracles(aggregator, [personas.Neil], 1, 1, 0) - await aggregator.connect(personas.Neil).submit(nextRound, answer) - }) - }) - }) - - describe('#setRequesterPermissions', () => { - beforeEach(async () => { - await addOracles(aggregator, [personas.Neil], 1, 1, 0) - - await aggregator.connect(personas.Neil).submit(nextRound, answer) - nextRound = nextRound + 1 - }) - - describe('when called by the owner', () => { - it('allows the specified address to start new rounds', async () => { - await aggregator.setRequesterPermissions( - await personas.Neil.getAddress(), - true, - 0, - ) - - await aggregator.connect(personas.Neil).requestNewRound() - }) - - it('emits a log announcing the update', async () => { - await expect( - aggregator.setRequesterPermissions( - await personas.Neil.getAddress(), - true, - 0, - ), - ) - .to.emit(aggregator, 'RequesterPermissionsSet') - .withArgs(await personas.Neil.getAddress(), true, 0) - }) - - describe('when the address is already authorized', () => { - beforeEach(async () => { - await aggregator.setRequesterPermissions( - await personas.Neil.getAddress(), - true, - 0, - ) - }) - - it('does not emit a log for already authorized accounts', async () => { - const tx = await aggregator.setRequesterPermissions( - await personas.Neil.getAddress(), - true, - 0, - ) - const receipt = await tx.wait() - assert.equal(0, receipt?.logs?.length) - }) - }) - - describe('when permission is removed by the owner', () => { - beforeEach(async () => { - await aggregator.setRequesterPermissions( - await personas.Neil.getAddress(), - true, - 0, - ) - }) - - it('does not allow the specified address to start new rounds', async () => { - await aggregator.setRequesterPermissions( - await personas.Neil.getAddress(), - false, - 0, - ) - - await evmRevert( - aggregator.connect(personas.Neil).requestNewRound(), - 'not authorized requester', - ) - }) - - it('emits a log announcing the update', async () => { - await expect( - aggregator.setRequesterPermissions( - await personas.Neil.getAddress(), - false, - 0, - ), - ) - .to.emit(aggregator, 'RequesterPermissionsSet') - .withArgs(await personas.Neil.getAddress(), false, 0) - }) - - it('does not emit a log for accounts without authorization', async () => { - const tx = await aggregator.setRequesterPermissions( - await personas.Ned.getAddress(), - false, - 0, - ) - const receipt = await tx.wait() - assert.equal(0, receipt?.logs?.length) - }) - }) - }) - - describe('when called by a stranger', () => { - it('reverts', async () => { - await evmRevert( - aggregator - .connect(personas.Neil) - .setRequesterPermissions(await personas.Neil.getAddress(), true, 0), - 'Only callable by owner', - ) - - await evmRevert( - aggregator.connect(personas.Neil).requestNewRound(), - 'not authorized requester', - ) - }) - }) - }) - - describe('#oracleRoundState', () => { - describe('when round ID 0 is passed in', () => { - const previousSubmission = 42 - let baseFunds: any - let minAnswers: number - let maxAnswers: number - let submitters: Signer[] - - beforeEach(async () => { - oracles = [ - personas.Neil, - personas.Ned, - personas.Nelly, - personas.Nancy, - personas.Norbert, - ] - minAnswers = 3 - maxAnswers = 4 - - await addOracles(aggregator, oracles, minAnswers, maxAnswers, rrDelay) - submitters = [ - personas.Nelly, - personas.Ned, - personas.Neil, - personas.Nancy, - ] - await advanceRound(aggregator, submitters, previousSubmission) - baseFunds = BigNumber.from(deposit).sub( - paymentAmount.mul(submitters.length), - ) - startingState = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - }) - - it('returns all of the important round information', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 2, - latestSubmission: previousSubmission, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds, - oracleCount: oracles.length, - paymentAmount, - }) - }) - - it('reverts if called by a contract', async () => { - testHelper = await testHelperFactory.connect(personas.Carol).deploy() - await evmRevert( - testHelper.readOracleRoundState( - aggregator.address, - await personas.Neil.getAddress(), - ), - 'off-chain reading only', - ) - }) - - describe('when the restart delay is not enforced', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { - minAnswers, - maxAnswers, - restartDelay: 0, - }) - }) - - describe('< min submissions and oracle not included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [personas.Neil]) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 2, - latestSubmission: previousSubmission, - startedAt: ShouldBeSet, - timeout, - availableFunds: baseFunds.sub(paymentAmount), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - - describe('< min submissions and oracle included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [personas.Nelly]) - }) - - it('is not eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 2, - latestSubmission: answer, - startedAt: ShouldBeSet, - timeout, - availableFunds: baseFunds.sub(paymentAmount), - oracleCount: oracles.length, - paymentAmount, - }) - }) - - describe('and timed out', () => { - beforeEach(async () => { - await increaseTimeBy(timeout + 1, ethers.provider) - await mineBlock(ethers.provider) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - - describe('>= min submissions and oracle not included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [ - personas.Neil, - personas.Nancy, - personas.Ned, - ]) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 2, - latestSubmission: previousSubmission, - startedAt: ShouldBeSet, - timeout, - availableFunds: baseFunds.sub(paymentAmount.mul(3)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - - describe('>= min submissions and oracle included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [ - personas.Neil, - personas.Nelly, - personas.Ned, - ]) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(3)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - - describe('and timed out', () => { - beforeEach(async () => { - await increaseTimeBy(timeout + 1, ethers.provider) - await mineBlock(ethers.provider) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(3)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - - describe('max submissions and oracle not included', () => { - beforeEach(async () => { - submitters = [ - personas.Neil, - personas.Ned, - personas.Nancy, - personas.Norbert, - ] - assert.equal( - submitters.length, - maxAnswers, - 'precondition, please update submitters if maxAnswers changes', - ) - await advanceRound(aggregator, submitters) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 3, - latestSubmission: previousSubmission, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(4)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - - describe('max submissions and oracle included', () => { - beforeEach(async () => { - submitters = [ - personas.Neil, - personas.Ned, - personas.Nelly, - personas.Nancy, - ] - assert.equal( - submitters.length, - maxAnswers, - 'precondition, please update submitters if maxAnswers changes', - ) - await advanceRound(aggregator, submitters) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(4)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - - describe('when the restart delay is enforced', () => { - beforeEach(async () => { - await updateFutureRounds(aggregator, { - minAnswers, - maxAnswers, - restartDelay: maxAnswers - 1, - }) - }) - - describe('< min submissions and oracle not included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [personas.Neil, personas.Ned]) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 2, - latestSubmission: previousSubmission, - startedAt: ShouldBeSet, - timeout, - availableFunds: baseFunds.sub(paymentAmount.mul(2)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - - describe('< min submissions and oracle included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [personas.Neil, personas.Nelly]) - }) - - it('is not eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 2, - latestSubmission: answer, - startedAt: ShouldBeSet, - timeout, - availableFunds: baseFunds.sub(paymentAmount.mul(2)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - - describe('and timed out', () => { - beforeEach(async () => { - await increaseTimeBy(timeout + 1, ethers.provider) - await mineBlock(ethers.provider) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(2)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - - describe('>= min submissions and oracle not included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [ - personas.Neil, - personas.Ned, - personas.Nancy, - ]) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 2, - latestSubmission: previousSubmission, - startedAt: ShouldBeSet, - timeout, - availableFunds: baseFunds.sub(paymentAmount.mul(3)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - - describe('>= min submissions and oracle included', () => { - beforeEach(async () => { - await advanceRound(aggregator, [ - personas.Neil, - personas.Ned, - personas.Nelly, - ]) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(3)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - - describe('and timed out', () => { - beforeEach(async () => { - await increaseTimeBy(timeout + 1, ethers.provider) - await mineBlock(ethers.provider) - }) - - it('is eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, // restart delay enforced - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(3)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - - describe('max submissions and oracle not included', () => { - beforeEach(async () => { - submitters = [ - personas.Neil, - personas.Ned, - personas.Nancy, - personas.Norbert, - ] - assert.equal( - submitters.length, - maxAnswers, - 'precondition, please update submitters if maxAnswers changes', - ) - await advanceRound(aggregator, submitters, answer) - }) - - it('is not eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 3, - latestSubmission: previousSubmission, - startedAt: ShouldNotBeSet, - timeout: 0, // details have been deleted - availableFunds: baseFunds.sub(paymentAmount.mul(4)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - - describe('max submissions and oracle included', () => { - beforeEach(async () => { - submitters = [ - personas.Neil, - personas.Ned, - personas.Nelly, - personas.Nancy, - ] - assert.equal( - submitters.length, - maxAnswers, - 'precondition, please update submitters if maxAnswers changes', - ) - await advanceRound(aggregator, submitters, answer) - }) - - it('is not eligible to submit', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 3, - latestSubmission: answer, - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: baseFunds.sub(paymentAmount.mul(4)), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - }) - - describe('when non-zero round ID 0 is passed in', () => { - const answers = [0, 42, 47, 52, 57] - let currentFunds: any - - beforeEach(async () => { - oracles = [personas.Neil, personas.Ned, personas.Nelly] - - await addOracles(aggregator, oracles, 2, 3, rrDelay) - startingState = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 0, - ) - await advanceRound(aggregator, oracles, answers[1]) - await advanceRound( - aggregator, - [personas.Neil, personas.Ned], - answers[2], - ) - await advanceRound(aggregator, oracles, answers[3]) - await advanceRound(aggregator, [personas.Neil], answers[4]) - const submissionsSoFar = 9 - currentFunds = BigNumber.from(deposit).sub( - paymentAmount.mul(submissionsSoFar), - ) - }) - - it('returns info about previous rounds', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 1, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 1, - latestSubmission: answers[3], - startedAt: ShouldBeSet, - timeout: 0, - availableFunds: currentFunds, - oracleCount: oracles.length, - paymentAmount: 0, - }) - }) - - it('returns info about previous rounds that were not submitted to', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 2, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 2, - latestSubmission: answers[3], - startedAt: ShouldBeSet, - timeout, - availableFunds: currentFunds, - oracleCount: oracles.length, - paymentAmount, - }) - }) - - describe('for the current round', () => { - describe('which has not been submitted to', () => { - it("returns info about the current round that hasn't been submitted to", async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 4, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 4, - latestSubmission: answers[3], - startedAt: ShouldBeSet, - timeout, - availableFunds: currentFunds, - oracleCount: oracles.length, - paymentAmount, - }) - }) - - it('returns info about the subsequent round', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 5, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 5, - latestSubmission: answers[3], - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: currentFunds, - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - - describe('which has been submitted to', () => { - beforeEach(async () => { - await aggregator.connect(personas.Nelly).submit(4, answers[4]) - }) - - it("returns info about the current round that hasn't been submitted to", async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 4, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 4, - latestSubmission: answers[4], - startedAt: ShouldBeSet, - timeout, - availableFunds: currentFunds.sub(paymentAmount), - oracleCount: oracles.length, - paymentAmount, - }) - }) - - it('returns info about the subsequent round', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 5, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: true, - roundId: 5, - latestSubmission: answers[4], - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: currentFunds.sub(paymentAmount), - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - - it('returns speculative info about future rounds', async () => { - const state = await aggregator.oracleRoundState( - await personas.Nelly.getAddress(), - 6, - ) - - await checkOracleRoundState(state, { - eligibleToSubmit: false, - roundId: 6, - latestSubmission: answers[3], - startedAt: ShouldNotBeSet, - timeout: 0, - availableFunds: currentFunds, - oracleCount: oracles.length, - paymentAmount, - }) - }) - }) - }) - - describe('#getRoundData', () => { - let latestRoundId: any - beforeEach(async () => { - oracles = [personas.Nelly] - const minMax = oracles.length - await addOracles(aggregator, oracles, minMax, minMax, rrDelay) - await advanceRound(aggregator, oracles, answer) - latestRoundId = await aggregator.latestRound() - }) - - it('returns the relevant round information', async () => { - const round = await aggregator.getRoundData(latestRoundId) - bigNumEquals(latestRoundId, round.roundId) - bigNumEquals(answer, round.answer) - const nowSeconds = new Date().valueOf() / 1000 - assert.isAbove(round.updatedAt.toNumber(), nowSeconds - 120) - bigNumEquals(round.updatedAt, round.startedAt) - bigNumEquals(latestRoundId, round.answeredInRound) - }) - - it('reverts if a round is not present', async () => { - await evmRevert( - aggregator.getRoundData(latestRoundId.add(1)), - 'No data present', - ) - }) - - it('reverts if a round ID is too big', async () => { - const overflowedId = BigNumber.from(2).pow(32).add(1) - - await evmRevert(aggregator.getRoundData(overflowedId), 'No data present') - }) - }) - - describe('#latestRoundData', () => { - beforeEach(async () => { - oracles = [personas.Nelly] - const minMax = oracles.length - await addOracles(aggregator, oracles, minMax, minMax, rrDelay) - }) - - describe('when an answer has already been received', () => { - beforeEach(async () => { - await advanceRound(aggregator, oracles, answer) - }) - - it('returns the relevant round info without reverting', async () => { - const round = await aggregator.latestRoundData() - const latestRoundId = await aggregator.latestRound() - - bigNumEquals(latestRoundId, round.roundId) - bigNumEquals(answer, round.answer) - const nowSeconds = new Date().valueOf() / 1000 - assert.isAbove(round.updatedAt.toNumber(), nowSeconds - 120) - bigNumEquals(round.updatedAt, round.startedAt) - bigNumEquals(latestRoundId, round.answeredInRound) - }) - }) - - it('reverts if a round is not present', async () => { - await evmRevert(aggregator.latestRoundData(), 'No data present') - }) - }) - - describe('#latestAnswer', () => { - beforeEach(async () => { - oracles = [personas.Nelly] - const minMax = oracles.length - await addOracles(aggregator, oracles, minMax, minMax, rrDelay) - }) - - describe('when an answer has already been received', () => { - beforeEach(async () => { - await advanceRound(aggregator, oracles, answer) - }) - - it('returns the latest answer without reverting', async () => { - bigNumEquals(answer, await aggregator.latestAnswer()) - }) - }) - - it('returns zero', async () => { - bigNumEquals(0, await aggregator.latestAnswer()) - }) - }) - - describe('#setValidator', () => { - beforeEach(async () => { - validator = await validatorMockFactory.connect(personas.Carol).deploy() - assert.equal(emptyAddress, await aggregator.validator()) - }) - - it('emits a log event showing the validator was changed', async () => { - await expect( - aggregator.connect(personas.Carol).setValidator(validator.address), - ) - .to.emit(aggregator, 'ValidatorUpdated') - .withArgs(emptyAddress, validator.address) - assert.equal(validator.address, await aggregator.validator()) - - await expect( - aggregator.connect(personas.Carol).setValidator(validator.address), - ).to.not.emit(aggregator, 'ValidatorUpdated') - assert.equal(validator.address, await aggregator.validator()) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - aggregator.connect(personas.Neil).setValidator(validator.address), - 'Only callable by owner', - ) - }) - }) - }) - - describe('integrating with historic deviation checker', () => { - let validator: Contract - let flags: Contract - let ac: Contract - const flaggingThreshold = 1000 // 1% - - beforeEach(async () => { - ac = await acFactory.connect(personas.Carol).deploy() - flags = await flagsFactory.connect(personas.Carol).deploy(ac.address) - validator = await validatorFactory - .connect(personas.Carol) - .deploy(flags.address, flaggingThreshold) - await ac.connect(personas.Carol).addAccess(validator.address) - - await aggregator.connect(personas.Carol).setValidator(validator.address) - - oracles = [personas.Nelly] - const minMax = oracles.length - await addOracles(aggregator, oracles, minMax, minMax, rrDelay) - }) - - it('raises a flag on with high enough deviation', async () => { - await aggregator.connect(personas.Nelly).submit(nextRound, 100) - nextRound++ - - await expect(aggregator.connect(personas.Nelly).submit(nextRound, 102)) - .to.emit(flags, 'FlagRaised') - .withArgs(aggregator.address) - }) - - it('does not raise a flag with low enough deviation', async () => { - await aggregator.connect(personas.Nelly).submit(nextRound, 100) - nextRound++ - - await expect( - aggregator.connect(personas.Nelly).submit(nextRound, 101), - ).to.not.emit(flags, 'FlagRaised') - }) - }) -}) diff --git a/contracts/test/v0.6/Median.test.ts b/contracts/test/v0.6/Median.test.ts deleted file mode 100644 index 8aea4722b6d..00000000000 --- a/contracts/test/v0.6/Median.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { ethers } from 'hardhat' -import { assert } from 'chai' -import { Signer, Contract, ContractFactory, BigNumber } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { bigNumEquals } from '../test-helpers/matchers' - -let defaultAccount: Signer -let medianTestHelperFactory: ContractFactory -before(async () => { - const personas: Personas = (await getUsers()).personas - defaultAccount = personas.Default - medianTestHelperFactory = await ethers.getContractFactory( - 'src/v0.6/tests/MedianTestHelper.sol:MedianTestHelper', - defaultAccount, - ) -}) - -describe('Median', () => { - let median: Contract - - beforeEach(async () => { - median = await medianTestHelperFactory.connect(defaultAccount).deploy() - }) - - describe('testing various lists', () => { - const tests = [ - { - name: 'ordered ascending', - responses: [0, 1, 2, 3, 4, 5, 6, 7], - want: 3, - }, - { - name: 'ordered descending', - responses: [7, 6, 5, 4, 3, 2, 1, 0], - want: 3, - }, - { - name: 'unordered length 1', - responses: [20], - want: 20, - }, - { - name: 'unordered length 2', - responses: [20, 0], - want: 10, - }, - { - name: 'unordered length 3', - responses: [20, 0, 16], - want: 16, - }, - { - name: 'unordered length 4', - responses: [20, 0, 15, 16], - want: 15, - }, - { - name: 'unordered length 7', - responses: [1001, 1, 101, 10, 11, 0, 111], - want: 11, - }, - { - name: 'unordered length 9', - responses: [8, 8, 4, 5, 5, 7, 9, 5, 9], - want: 7, - }, - { - name: 'unordered long', - responses: [33, 44, 89, 101, 67, 7, 23, 55, 88, 324, 0, 88], - want: 61, // 67 + 55 / 2 - }, - { - name: 'unordered longer', - responses: [ - 333121, 323453, 337654, 345363, 345363, 333456, 335477, 333323, - 332352, 354648, 983260, 333856, 335468, 376987, 333253, 388867, - 337879, 333324, 338678, - ], - want: 335477, - }, - { - name: 'overflowing numbers', - responses: [ - BigNumber.from( - '57896044618658097711785492504343953926634992332820282019728792003956564819967', - ), - BigNumber.from( - '57896044618658097711785492504343953926634992332820282019728792003956564819967', - ), - ], - want: BigNumber.from( - '57896044618658097711785492504343953926634992332820282019728792003956564819967', - ), - }, - { - name: 'overflowing numbers', - responses: [ - BigNumber.from( - '57896044618658097711785492504343953926634992332820282019728792003956564819967', - ), - BigNumber.from( - '57896044618658097711785492504343953926634992332820282019728792003956564819966', - ), - ], - want: BigNumber.from( - '57896044618658097711785492504343953926634992332820282019728792003956564819966', - ), - }, - { - name: 'really long', - responses: [ - 56, 2, 31, 33, 55, 38, 35, 12, 41, 47, 21, 22, 40, 39, 10, 32, 49, 3, - 54, 45, 53, 14, 20, 59, 1, 30, 24, 6, 5, 37, 58, 51, 46, 17, 29, 7, - 27, 9, 43, 8, 34, 42, 28, 23, 57, 0, 11, 48, 52, 50, 15, 16, 26, 25, - 4, 36, 19, 44, 18, 13, - ], - want: 29, - }, - ] - - for (const test of tests) { - it(test.name, async () => { - bigNumEquals(test.want, await median.publicGet(test.responses)) - }) - } - }) - - // long running (minutes) exhaustive test. - // skipped because very slow, but useful for thorough validation - xit('permutations', async () => { - const permutations = (list: number[]) => { - const result: number[][] = [] - const used: number[] = [] - - const permute = (unused: number[]) => { - if (unused.length == 0) { - result.push([...used]) - return - } - - for (let i = 0; i < unused.length; i++) { - const elem = unused.splice(i, 1)[0] - used.push(elem) - permute(unused) - unused.splice(i, 0, elem) - used.pop() - } - } - - permute(list) - return result - } - - { - const list = [0, 2, 5, 7, 8, 10] - for (const permuted of permutations(list)) { - for (let i = 0; i < list.length; i++) { - for (let j = 0; j < list.length; j++) { - if (i < j) { - const foo = await median.publicQuickselectTwo(permuted, i, j) - bigNumEquals(list[i], foo[0]) - bigNumEquals(list[j], foo[1]) - } - } - } - } - } - - { - const list = [0, 1, 1, 1, 2] - for (const permuted of permutations(list)) { - for (let i = 0; i < list.length; i++) { - for (let j = 0; j < list.length; j++) { - if (i < j) { - const foo = await median.publicQuickselectTwo(permuted, i, j) - bigNumEquals(list[i], foo[0]) - bigNumEquals(list[j], foo[1]) - } - } - } - } - } - }) - - // Checks the validity of the sorting network in `shortList` - describe('validate sorting network', () => { - const net = [ - [0, 1], - [2, 3], - [4, 5], - [0, 2], - [1, 3], - [4, 6], - [1, 2], - [5, 6], - [0, 4], - [1, 5], - [2, 6], - [1, 4], - [3, 6], - [2, 4], - [3, 5], - [3, 4], - ] - - // See: https://en.wikipedia.org/wiki/Sorting_network#Zero-one_principle - xit('zero-one principle', async () => { - const sortWithNet = (list: number[]) => { - for (const [i, j] of net) { - if (list[i] > list[j]) { - ;[list[i], list[j]] = [list[j], list[i]] - } - } - } - - for (let n = 0; n < (1 << 7) - 1; n++) { - const list = [ - (n >> 6) & 1, - (n >> 5) & 1, - (n >> 4) & 1, - (n >> 3) & 1, - (n >> 2) & 1, - (n >> 1) & 1, - (n >> 0) & 1, - ] - const sum = list.reduce((a, b) => a + b, 0) - sortWithNet(list) - const sortedSum = list.reduce((a, b) => a + b, 0) - assert.equal(sortedSum, sum, 'Number of zeros and ones changed') - list.reduce((switched, i) => { - assert.isTrue(!switched || i != 0, 'error at n=' + n.toString()) - return i != 0 - }, false) - } - }) - }) -}) diff --git a/contracts/test/v0.6/Owned.test.ts b/contracts/test/v0.6/Owned.test.ts deleted file mode 100644 index f522b9c44c9..00000000000 --- a/contracts/test/v0.6/Owned.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { Signer, Contract, ContractFactory } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' - -let personas: Personas - -let owner: Signer -let nonOwner: Signer -let newOwner: Signer - -let ownedFactory: ContractFactory -let owned: Contract - -before(async () => { - personas = (await getUsers()).personas - owner = personas.Carol - nonOwner = personas.Neil - newOwner = personas.Ned - ownedFactory = await ethers.getContractFactory( - 'src/v0.6/Owned.sol:Owned', - owner, - ) -}) - -describe('Owned', () => { - beforeEach(async () => { - owned = await ownedFactory.connect(owner).deploy() - }) - - it('has a limited public interface [ @skip-coverage ]', async () => { - publicAbi(owned, ['acceptOwnership', 'owner', 'transferOwnership']) - }) - - describe('#constructor', () => { - it('assigns ownership to the deployer', async () => { - const [actual, expected] = await Promise.all([ - owner.getAddress(), - owned.owner(), - ]) - - assert.equal(actual, expected) - }) - }) - - describe('#transferOwnership', () => { - describe('when called by an owner', () => { - it('emits a log', async () => { - await expect( - owned.connect(owner).transferOwnership(await newOwner.getAddress()), - ) - .to.emit(owned, 'OwnershipTransferRequested') - .withArgs(await owner.getAddress(), await newOwner.getAddress()) - }) - }) - }) - - describe('when called by anyone but the owner', () => { - it('reverts', async () => - await expect( - owned.connect(nonOwner).transferOwnership(await newOwner.getAddress()), - ).to.be.reverted) - }) - - describe('#acceptOwnership', () => { - describe('after #transferOwnership has been called', () => { - beforeEach(async () => { - await owned - .connect(owner) - .transferOwnership(await newOwner.getAddress()) - }) - - it('allows the recipient to call it', async () => { - await expect(owned.connect(newOwner).acceptOwnership()) - .to.emit(owned, 'OwnershipTransferred') - .withArgs(await owner.getAddress(), await newOwner.getAddress()) - }) - - it('does not allow a non-recipient to call it', async () => - await expect(owned.connect(nonOwner).acceptOwnership()).to.be.reverted) - }) - }) -}) diff --git a/contracts/test/v0.6/SignedSafeMath.test.ts b/contracts/test/v0.6/SignedSafeMath.test.ts deleted file mode 100644 index e942f64d6b5..00000000000 --- a/contracts/test/v0.6/SignedSafeMath.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { ethers } from 'hardhat' -import { expect } from 'chai' -import { Signer, Contract, ContractFactory, BigNumber } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { bigNumEquals } from '../test-helpers/matchers' - -let defaultAccount: Signer -let concreteSignedSafeMathFactory: ContractFactory - -before(async () => { - const personas: Personas = (await getUsers()).personas - defaultAccount = personas.Default - concreteSignedSafeMathFactory = await ethers.getContractFactory( - 'src/v0.6/tests/ConcreteSignedSafeMath.sol:ConcreteSignedSafeMath', - defaultAccount, - ) -}) - -describe('SignedSafeMath', () => { - // a version of the adder contract where we make all ABI exposed functions constant - // TODO: submit upstream PR to support constant contract type generation - let adder: Contract - let response: BigNumber - - const INT256_MAX = BigNumber.from( - '57896044618658097711785492504343953926634992332820282019728792003956564819967', - ) - const INT256_MIN = BigNumber.from( - '-57896044618658097711785492504343953926634992332820282019728792003956564819968', - ) - - beforeEach(async () => { - adder = await concreteSignedSafeMathFactory.connect(defaultAccount).deploy() - }) - - describe('#add', () => { - describe('given a positive and a positive', () => { - it('works', async () => { - response = await adder.testAdd(1, 2) - bigNumEquals(3, response) - }) - - it('works with zero', async () => { - response = await adder.testAdd(INT256_MAX, 0) - bigNumEquals(INT256_MAX, response) - }) - - describe('when both are large enough to overflow', () => { - it('throws', async () => { - await expect(adder.testAdd(INT256_MAX, 1)).to.be.revertedWith( - 'SignedSafeMath: addition overflow', - ) - }) - }) - }) - - describe('given a negative and a negative', () => { - it('works', async () => { - response = await adder.testAdd(-1, -2) - bigNumEquals(-3, response) - }) - - it('works with zero', async () => { - response = await adder.testAdd(INT256_MIN, 0) - bigNumEquals(INT256_MIN, response) - }) - - describe('when both are large enough to overflow', () => { - it('throws', async () => { - await expect(adder.testAdd(INT256_MIN, -1)).to.be.revertedWith( - 'SignedSafeMath: addition overflow', - ) - }) - }) - }) - - describe('given a positive and a negative', () => { - it('works', async () => { - response = await adder.testAdd(1, -2) - bigNumEquals(-1, response) - }) - }) - - describe('given a negative and a positive', () => { - it('works', async () => { - response = await adder.testAdd(-1, 2) - bigNumEquals(1, response) - }) - }) - }) - - describe('#avg', () => { - describe('given a positive and a positive', () => { - it('works', async () => { - response = await adder.testAvg(2, 4) - bigNumEquals(3, response) - }) - - it('works with zero', async () => { - response = await adder.testAvg(0, 4) - bigNumEquals(2, response) - response = await adder.testAvg(4, 0) - bigNumEquals(2, response) - }) - - it('works with large numbers', async () => { - response = await adder.testAvg(INT256_MAX, INT256_MAX) - bigNumEquals(INT256_MAX, response) - }) - - it('rounds towards zero', async () => { - response = await adder.testAvg(1, 2) - bigNumEquals(1, response) - }) - }) - - describe('given a negative and a negative', () => { - it('works', async () => { - response = await adder.testAvg(-2, -4) - bigNumEquals(-3, response) - }) - - it('works with zero', async () => { - response = await adder.testAvg(0, -4) - bigNumEquals(-2, response) - response = await adder.testAvg(-4, 0) - bigNumEquals(-2, response) - }) - - it('works with large numbers', async () => { - response = await adder.testAvg(INT256_MIN, INT256_MIN) - bigNumEquals(INT256_MIN, response) - }) - - it('rounds towards zero', async () => { - response = await adder.testAvg(-1, -2) - bigNumEquals(-1, response) - }) - }) - - describe('given a positive and a negative', () => { - it('works', async () => { - response = await adder.testAvg(2, -4) - bigNumEquals(-1, response) - response = await adder.testAvg(4, -2) - bigNumEquals(1, response) - }) - - it('works with large numbers', async () => { - response = await adder.testAvg(INT256_MAX, -2) - bigNumEquals(INT256_MAX.sub(2).div(2), response) - response = await adder.testAvg(INT256_MAX, INT256_MIN) - bigNumEquals(0, response) - }) - - it('rounds towards zero', async () => { - response = await adder.testAvg(1, -4) - bigNumEquals(-1, response) - response = await adder.testAvg(4, -1) - bigNumEquals(1, response) - }) - }) - - describe('given a negative and a positive', () => { - it('works', async () => { - response = await adder.testAvg(-2, 4) - bigNumEquals(1, response) - response = await adder.testAvg(-4, 2) - bigNumEquals(-1, response) - }) - - it('works with large numbers', async () => { - response = await adder.testAvg(INT256_MIN, 2) - bigNumEquals(INT256_MIN.add(2).div(2), response) - response = await adder.testAvg(INT256_MIN, INT256_MAX) - bigNumEquals(0, response) - }) - - it('rounds towards zero', async () => { - response = await adder.testAvg(-1, 4) - bigNumEquals(1, response) - response = await adder.testAvg(-4, 1) - bigNumEquals(-1, response) - }) - }) - }) -}) diff --git a/contracts/test/v0.6/SimpleReadAccessController.test.ts b/contracts/test/v0.6/SimpleReadAccessController.test.ts deleted file mode 100644 index 7b76bc38cad..00000000000 --- a/contracts/test/v0.6/SimpleReadAccessController.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { Contract, ContractFactory, Transaction } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' - -let personas: Personas - -let controllerFactory: ContractFactory -let controller: Contract - -before(async () => { - personas = (await getUsers()).personas - controllerFactory = await ethers.getContractFactory( - 'src/v0.6/SimpleReadAccessController.sol:SimpleReadAccessController', - personas.Carol, - ) -}) - -describe('SimpleReadAccessController', () => { - beforeEach(async () => { - controller = await controllerFactory.connect(personas.Carol).deploy() - }) - - it('has a limited public interface [ @skip-coverage ]', async () => { - publicAbi(controller, [ - 'hasAccess', - 'addAccess', - 'disableAccessCheck', - 'enableAccessCheck', - 'removeAccess', - 'checkEnabled', - // Owned - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('#constructor', () => { - it('defaults checkEnabled to true', async () => { - assert(await controller.checkEnabled()) - }) - }) - - describe('#hasAccess', () => { - it('allows unauthorized calls originating from the same account', async () => { - assert.isTrue( - await controller - .connect(personas.Eddy) - .hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('blocks unauthorized calls originating from different accounts', async () => { - assert.isFalse( - await controller - .connect(personas.Carol) - .hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - assert.isFalse( - await controller - .connect(personas.Eddy) - .hasAccess(await personas.Carol.getAddress(), '0x00'), - ) - }) - }) - - describe('#addAccess', () => { - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller - .connect(personas.Eddy) - .addAccess(await personas.Eddy.getAddress()), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - assert.isFalse( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - tx = await controller.addAccess(await personas.Eddy.getAddress()) - }) - - it('adds the address to the controller', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - await expect(tx) - .to.emit(controller, 'AddedAccess') - .withArgs(await personas.Eddy.getAddress()) - }) - - describe('when called twice', () => { - it('does not emit a log', async () => { - const tx2 = await controller.addAccess( - await personas.Eddy.getAddress(), - ) - const receipt = await tx2.wait() - assert.equal(receipt.events?.length, 0) - }) - }) - }) - }) - - describe('#removeAccess', () => { - beforeEach(async () => { - await controller.addAccess(await personas.Eddy.getAddress()) - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller - .connect(personas.Eddy) - .removeAccess(await personas.Eddy.getAddress()), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - tx = await controller.removeAccess(await personas.Eddy.getAddress()) - }) - - it('removes the address from the controller', async () => { - assert.isFalse( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - await expect(tx) - .to.emit(controller, 'RemovedAccess') - .withArgs(await personas.Eddy.getAddress()) - }) - - describe('when called twice', () => { - it('does not emit a log', async () => { - const tx2 = await controller.removeAccess( - await personas.Eddy.getAddress(), - ) - const receipt = await tx2.wait() - assert.equal(receipt.events?.length, 0) - }) - }) - }) - }) - - describe('#disableAccessCheck', () => { - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller.connect(personas.Eddy).disableAccessCheck(), - ).to.be.revertedWith('Only callable by owner') - assert.isTrue(await controller.checkEnabled()) - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - await controller.addAccess(await personas.Eddy.getAddress()) - tx = await controller.disableAccessCheck() - }) - - it('sets checkEnabled to false', async () => { - assert.isFalse(await controller.checkEnabled()) - }) - - it('allows users with access', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('allows users without access', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Ned.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - await expect(tx).to.emit(controller, 'CheckAccessDisabled') - }) - - describe('when called twice', () => { - it('does not emit a log', async () => { - const tx2 = await controller.disableAccessCheck() - const receipt = await tx2.wait() - assert.equal(receipt.events?.length, 0) - }) - }) - }) - }) - - describe('#enableAccessCheck', () => { - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller.connect(personas.Eddy).enableAccessCheck(), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - await controller.disableAccessCheck() - await controller.addAccess(await personas.Eddy.getAddress()) - tx = await controller.enableAccessCheck() - }) - - it('allows users with access', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('does not allow users without access', async () => { - assert.isFalse( - await controller.hasAccess(await personas.Ned.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - expect(tx).to.emit(controller, 'CheckAccessEnabled') - }) - - describe('when called twice', () => { - it('does not emit a log', async () => { - const tx2 = await controller.enableAccessCheck() - const receipt = await tx2.wait() - assert.equal(receipt.events?.length, 0) - }) - }) - }) - }) -}) diff --git a/contracts/test/v0.6/SimpleWriteAccessController.test.ts b/contracts/test/v0.6/SimpleWriteAccessController.test.ts deleted file mode 100644 index ae6c1691f91..00000000000 --- a/contracts/test/v0.6/SimpleWriteAccessController.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { Contract, ContractFactory, Transaction } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' - -let personas: Personas - -let controllerFactory: ContractFactory -let controller: Contract - -before(async () => { - personas = (await getUsers()).personas - controllerFactory = await ethers.getContractFactory( - 'src/v0.6/SimpleWriteAccessController.sol:SimpleWriteAccessController', - personas.Carol, - ) -}) - -describe('SimpleWriteAccessController', () => { - beforeEach(async () => { - controller = await controllerFactory.connect(personas.Carol).deploy() - }) - - it('has a limited public interface [ @skip-coverage ]', async () => { - publicAbi(controller, [ - 'hasAccess', - 'addAccess', - 'disableAccessCheck', - 'enableAccessCheck', - 'removeAccess', - 'checkEnabled', - // Owned - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('#constructor', () => { - it('defaults checkEnabled to true', async () => { - assert(await controller.checkEnabled()) - }) - }) - - describe('#hasAccess', () => { - it('allows unauthorized calls originating from the same account', async () => { - assert.isFalse( - await controller - .connect(personas.Eddy) - .hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('blocks unauthorized calls originating from different accounts', async () => { - assert.isFalse( - await controller - .connect(personas.Carol) - .hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - assert.isFalse( - await controller - .connect(personas.Eddy) - .hasAccess(await personas.Carol.getAddress(), '0x00'), - ) - }) - }) - - describe('#addAccess', () => { - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller - .connect(personas.Eddy) - .addAccess(await personas.Eddy.getAddress()), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - assert.isFalse( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - tx = await controller.addAccess(await personas.Eddy.getAddress()) - }) - - it('adds the address to the controller', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - expect(tx) - .to.emit(controller, 'AddedAccess') - .withArgs(await personas.Eddy.getAddress()) - }) - }) - }) - - describe('#removeAccess', () => { - beforeEach(async () => { - await controller.addAccess(await personas.Eddy.getAddress()) - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller - .connect(personas.Eddy) - .removeAccess(await personas.Eddy.getAddress()), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - tx = await controller.removeAccess(await personas.Eddy.getAddress()) - }) - - it('removes the address from the controller', async () => { - assert.isFalse( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - expect(tx) - .to.emit(controller, 'RemovedAccess') - .withArgs(await personas.Eddy.getAddress()) - }) - }) - }) - - describe('#disableAccessCheck', () => { - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller.connect(personas.Eddy).disableAccessCheck(), - ).to.be.revertedWith('Only callable by owner') - assert.isTrue(await controller.checkEnabled()) - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - await controller.addAccess(await personas.Eddy.getAddress()) - tx = await controller.disableAccessCheck() - }) - - it('sets checkEnabled to false', async () => { - assert.isFalse(await controller.checkEnabled()) - }) - - it('allows users with access', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('allows users without access', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Ned.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - await expect(tx).to.emit(controller, 'CheckAccessDisabled') - }) - }) - }) - - describe('#enableAccessCheck', () => { - describe('when called by a non-owner', () => { - it('reverts', async () => { - await expect( - controller.connect(personas.Eddy).enableAccessCheck(), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('when called by the owner', () => { - let tx: Transaction - beforeEach(async () => { - await controller.disableAccessCheck() - await controller.addAccess(await personas.Eddy.getAddress()) - tx = await controller.enableAccessCheck() - }) - - it('allows users with access', async () => { - assert.isTrue( - await controller.hasAccess(await personas.Eddy.getAddress(), '0x00'), - ) - }) - - it('does not allow users without access', async () => { - assert.isFalse( - await controller.hasAccess(await personas.Ned.getAddress(), '0x00'), - ) - }) - - it('announces the change via a log', async () => { - await expect(tx).to.emit(controller, 'CheckAccessEnabled') - }) - }) - }) -}) diff --git a/contracts/test/v0.6/VRFD20.test.ts b/contracts/test/v0.6/VRFD20.test.ts deleted file mode 100644 index 77141be6230..00000000000 --- a/contracts/test/v0.6/VRFD20.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { ethers } from 'hardhat' -import { assert, expect } from 'chai' -import { - BigNumber, - constants, - Contract, - ContractFactory, - ContractTransaction, -} from 'ethers' -import { getUsers, Personas, Roles } from '../test-helpers/setup' -import { - evmWordToAddress, - getLog, - publicAbi, - toBytes32String, - toWei, - numToBytes32, - getLogs, -} from '../test-helpers/helpers' - -let roles: Roles -let personas: Personas -let linkTokenFactory: ContractFactory -let vrfCoordinatorMockFactory: ContractFactory -let vrfD20Factory: ContractFactory - -before(async () => { - const users = await getUsers() - - roles = users.roles - personas = users.personas - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) - vrfCoordinatorMockFactory = await ethers.getContractFactory( - 'src/v0.6/tests/VRFCoordinatorMock.sol:VRFCoordinatorMock', - roles.defaultAccount, - ) - vrfD20Factory = await ethers.getContractFactory( - 'src/v0.6/examples/VRFD20.sol:VRFD20', - roles.defaultAccount, - ) -}) - -describe('VRFD20', () => { - const deposit = toWei('1') - const fee = toWei('0.1') - const keyHash = toBytes32String('keyHash') - - let link: Contract - let vrfCoordinator: Contract - let vrfD20: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - vrfCoordinator = await vrfCoordinatorMockFactory - .connect(roles.defaultAccount) - .deploy(link.address) - vrfD20 = await vrfD20Factory - .connect(roles.defaultAccount) - .deploy(vrfCoordinator.address, link.address, keyHash, fee) - await link.transfer(vrfD20.address, deposit) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(vrfD20, [ - // Owned - 'acceptOwnership', - 'owner', - 'transferOwnership', - //VRFConsumerBase - 'rawFulfillRandomness', - // VRFD20 - 'rollDice', - 'house', - 'withdrawLINK', - 'keyHash', - 'fee', - 'setKeyHash', - 'setFee', - ]) - }) - - describe('#withdrawLINK', () => { - describe('failure', () => { - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20 - .connect(roles.stranger) - .withdrawLINK(await roles.stranger.getAddress(), deposit), - ).to.be.revertedWith('Only callable by owner') - }) - - it('reverts when not enough LINK in the contract', async () => { - const withdrawAmount = deposit.mul(2) - await expect( - vrfD20 - .connect(roles.defaultAccount) - .withdrawLINK( - await roles.defaultAccount.getAddress(), - withdrawAmount, - ), - ).to.be.reverted - }) - }) - - describe('success', () => { - it('withdraws LINK', async () => { - const startingAmount = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - const expectedAmount = BigNumber.from(startingAmount).add(deposit) - await vrfD20 - .connect(roles.defaultAccount) - .withdrawLINK(await roles.defaultAccount.getAddress(), deposit) - const actualAmount = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - assert.equal(actualAmount.toString(), expectedAmount.toString()) - }) - }) - }) - - describe('#setKeyHash', () => { - const newHash = toBytes32String('newhash') - - describe('failure', () => { - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20.connect(roles.stranger).setKeyHash(newHash), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('success', () => { - it('sets the key hash', async () => { - await vrfD20.setKeyHash(newHash) - const actualHash = await vrfD20.keyHash() - assert.equal(actualHash, newHash) - }) - }) - }) - - describe('#setFee', () => { - const newFee = 1234 - - describe('failure', () => { - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20.connect(roles.stranger).setFee(newFee), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('success', () => { - it('sets the fee', async () => { - await vrfD20.setFee(newFee) - const actualFee = await vrfD20.fee() - assert.equal(actualFee.toString(), newFee.toString()) - }) - }) - }) - - describe('#house', () => { - describe('failure', () => { - it('reverts when dice not rolled', async () => { - await expect( - vrfD20.house(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Dice not rolled') - }) - - it('reverts when dice roll is in progress', async () => { - await vrfD20.rollDice(await personas.Nancy.getAddress()) - await expect( - vrfD20.house(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Roll in progress') - }) - }) - - describe('success', () => { - it('returns the correct house', async () => { - const randomness = 98765 - const expectedHouse = 'Martell' - const tx = await vrfD20.rollDice(await personas.Nancy.getAddress()) - const log = await getLog(tx, 3) - const eventRequestId = log?.topics?.[1] - await vrfCoordinator.callBackWithRandomness( - eventRequestId, - randomness, - vrfD20.address, - ) - const response = await vrfD20.house(await personas.Nancy.getAddress()) - assert.equal(response.toString(), expectedHouse) - }) - }) - }) - - describe('#rollDice', () => { - describe('success', () => { - let tx: ContractTransaction - beforeEach(async () => { - tx = await vrfD20.rollDice(await personas.Nancy.getAddress()) - }) - - it('emits a RandomnessRequest event from the VRFCoordinator', async () => { - const log = await getLog(tx, 2) - const topics = log?.topics - assert.equal(evmWordToAddress(topics?.[1]), vrfD20.address) - assert.equal(topics?.[2], keyHash) - assert.equal(topics?.[3], constants.HashZero) - }) - }) - - describe('failure', () => { - it('reverts when LINK balance is zero', async () => { - const vrfD202 = await vrfD20Factory - .connect(roles.defaultAccount) - .deploy(vrfCoordinator.address, link.address, keyHash, fee) - await expect( - vrfD202.rollDice(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Not enough LINK to pay fee') - }) - - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20 - .connect(roles.stranger) - .rollDice(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Only callable by owner') - }) - - it('reverts when the roller rolls more than once', async () => { - await vrfD20.rollDice(await personas.Nancy.getAddress()) - await expect( - vrfD20.rollDice(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Already rolled') - }) - }) - }) - - describe('#fulfillRandomness', () => { - const randomness = 98765 - const expectedModResult = (randomness % 20) + 1 - const expectedHouse = 'Martell' - let eventRequestId: string - beforeEach(async () => { - const tx = await vrfD20.rollDice(await personas.Nancy.getAddress()) - const log = await getLog(tx, 3) - eventRequestId = log?.topics?.[1] - }) - - describe('success', () => { - let tx: ContractTransaction - beforeEach(async () => { - tx = await vrfCoordinator.callBackWithRandomness( - eventRequestId, - randomness, - vrfD20.address, - ) - }) - - it('emits a DiceLanded event', async () => { - const log = await getLog(tx, 0) - assert.equal(log?.topics[1], eventRequestId) - assert.equal(log?.topics[2], numToBytes32(expectedModResult)) - }) - - it('sets the correct dice roll result', async () => { - const response = await vrfD20.house(await personas.Nancy.getAddress()) - assert.equal(response.toString(), expectedHouse) - }) - - it('allows someone else to roll', async () => { - const secondRandomness = 55555 - tx = await vrfD20.rollDice(await personas.Ned.getAddress()) - const log = await getLog(tx, 3) - eventRequestId = log?.topics?.[1] - tx = await vrfCoordinator.callBackWithRandomness( - eventRequestId, - secondRandomness, - vrfD20.address, - ) - }) - }) - - describe('failure', () => { - it('does not fulfill when fulfilled by the wrong VRFcoordinator', async () => { - const vrfCoordinator2 = await vrfCoordinatorMockFactory - .connect(roles.defaultAccount) - .deploy(link.address) - - const tx = await vrfCoordinator2.callBackWithRandomness( - eventRequestId, - randomness, - vrfD20.address, - ) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - }) -}) diff --git a/contracts/test/v0.7/AggregatorProxy.test.ts b/contracts/test/v0.7/AggregatorProxy.test.ts deleted file mode 100644 index 6e8ee41983d..00000000000 --- a/contracts/test/v0.7/AggregatorProxy.test.ts +++ /dev/null @@ -1,743 +0,0 @@ -import { ethers } from 'hardhat' -import { - increaseTimeBy, - numToBytes32, - publicAbi, - toWei, -} from '../test-helpers/helpers' -import { assert } from 'chai' -import { BigNumber, constants, Contract, ContractFactory, Signer } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { bigNumEquals, evmRevert } from '../test-helpers/matchers' - -let personas: Personas -let defaultAccount: Signer - -let linkTokenFactory: ContractFactory -let aggregatorFactory: ContractFactory -let historicAggregatorFactory: ContractFactory -let aggregatorFacadeFactory: ContractFactory -let aggregatorProxyFactory: ContractFactory -let fluxAggregatorFactory: ContractFactory -let reverterFactory: ContractFactory - -before(async () => { - const users = await getUsers() - - personas = users.personas - defaultAccount = users.roles.defaultAccount - - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - defaultAccount, - ) - aggregatorFactory = await ethers.getContractFactory( - 'src/v0.7/tests/MockV3Aggregator.sol:MockV3Aggregator', - defaultAccount, - ) - historicAggregatorFactory = await ethers.getContractFactory( - 'src/v0.7/tests/MockV2Aggregator.sol:MockV2Aggregator', - defaultAccount, - ) - aggregatorFacadeFactory = await ethers.getContractFactory( - 'src/v0.6/AggregatorFacade.sol:AggregatorFacade', - defaultAccount, - ) - historicAggregatorFactory = await ethers.getContractFactory( - 'src/v0.7/tests/MockV2Aggregator.sol:MockV2Aggregator', - defaultAccount, - ) - aggregatorFacadeFactory = await ethers.getContractFactory( - 'src/v0.6/AggregatorFacade.sol:AggregatorFacade', - defaultAccount, - ) - aggregatorProxyFactory = await ethers.getContractFactory( - 'src/v0.7/dev/AggregatorProxy.sol:AggregatorProxy', - defaultAccount, - ) - fluxAggregatorFactory = await ethers.getContractFactory( - 'src/v0.6/FluxAggregator.sol:FluxAggregator', - defaultAccount, - ) - reverterFactory = await ethers.getContractFactory( - 'src/v0.6/tests/Reverter.sol:Reverter', - defaultAccount, - ) -}) - -describe('AggregatorProxy', () => { - const deposit = toWei('100') - const response = numToBytes32(54321) - const response2 = numToBytes32(67890) - const decimals = 18 - const phaseBase = BigNumber.from(2).pow(64) - - let link: Contract - let aggregator: Contract - let aggregator2: Contract - let historicAggregator: Contract - let proxy: Contract - let flux: Contract - let reverter: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(defaultAccount).deploy() - aggregator = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response) - await link.transfer(aggregator.address, deposit) - proxy = await aggregatorProxyFactory - .connect(defaultAccount) - .deploy(aggregator.address) - const emptyAddress = constants.AddressZero - flux = await fluxAggregatorFactory - .connect(personas.Carol) - .deploy(link.address, 0, 0, emptyAddress, 0, 0, 18, 'TEST / LINK') - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(proxy, [ - 'aggregator', - 'confirmAggregator', - 'decimals', - 'description', - 'getAnswer', - 'getRoundData', - 'getTimestamp', - 'latestAnswer', - 'latestRound', - 'latestRoundData', - 'latestTimestamp', - 'phaseAggregators', - 'phaseId', - 'proposeAggregator', - 'proposedAggregator', - 'proposedGetRoundData', - 'proposedLatestRoundData', - 'version', - // Ownable methods: - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('constructor', () => { - it('sets the proxy phase and aggregator', async () => { - bigNumEquals(1, await proxy.phaseId()) - assert.equal(aggregator.address, await proxy.phaseAggregators(1)) - }) - }) - - describe('#latestRound', () => { - it('pulls the rate from the aggregator', async () => { - bigNumEquals(phaseBase.add(1), await proxy.latestRound()) - }) - }) - - describe('#latestAnswer', () => { - it('pulls the rate from the aggregator', async () => { - bigNumEquals(response, await proxy.latestAnswer()) - const latestRound = await proxy.latestRound() - bigNumEquals(response, await proxy.getAnswer(latestRound)) - }) - - describe('after being updated to another contract', () => { - beforeEach(async () => { - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - await link.transfer(aggregator2.address, deposit) - bigNumEquals(response2, await aggregator2.latestAnswer()) - - await proxy.proposeAggregator(aggregator2.address) - await proxy.confirmAggregator(aggregator2.address) - }) - - it('pulls the rate from the new aggregator', async () => { - bigNumEquals(response2, await proxy.latestAnswer()) - const latestRound = await proxy.latestRound() - bigNumEquals(response2, await proxy.getAnswer(latestRound)) - }) - }) - - describe('when the relevant info is not available', () => { - beforeEach(async () => { - await proxy.proposeAggregator(flux.address) - await proxy.confirmAggregator(flux.address) - }) - - it('does not revert when called with a non existent ID', async () => { - const actual = await proxy.latestAnswer() - bigNumEquals(0, actual) - }) - }) - }) - - describe('#getAnswer', () => { - describe('when the relevant round is not available', () => { - beforeEach(async () => { - await proxy.proposeAggregator(flux.address) - await proxy.confirmAggregator(flux.address) - }) - - it('does not revert when called with a non existent ID', async () => { - const proxyId = phaseBase.mul(await proxy.phaseId()).add(1) - const actual = await proxy.getAnswer(proxyId) - bigNumEquals(0, actual) - }) - }) - - describe('when the answer reverts in a non-predicted way', () => { - it('reverts', async () => { - reverter = await reverterFactory.connect(defaultAccount).deploy() - await proxy.proposeAggregator(reverter.address) - await proxy.confirmAggregator(reverter.address) - assert.equal(reverter.address, await proxy.aggregator()) - - const proxyId = phaseBase.mul(await proxy.phaseId()) - - await evmRevert(proxy.getAnswer(proxyId), 'Raised by Reverter.sol') - }) - }) - - describe('after being updated to another contract', () => { - let preUpdateRoundId: BigNumber - let preUpdateAnswer: BigNumber - - beforeEach(async () => { - preUpdateRoundId = await proxy.latestRound() - preUpdateAnswer = await proxy.latestAnswer() - - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - await link.transfer(aggregator2.address, deposit) - bigNumEquals(response2, await aggregator2.latestAnswer()) - - await proxy.proposeAggregator(aggregator2.address) - await proxy.confirmAggregator(aggregator2.address) - }) - - it('reports answers for previous phases', async () => { - const actualAnswer = await proxy.getAnswer(preUpdateRoundId) - bigNumEquals(preUpdateAnswer, actualAnswer) - }) - }) - - describe('when the relevant info is not available', () => { - it('returns 0', async () => { - const actual = await proxy.getAnswer(phaseBase.mul(777)) - bigNumEquals(0, actual) - }) - }) - - describe('when the round ID is too large', () => { - const overflowRoundId = BigNumber.from(2) - .pow(255) - .add(phaseBase) // get the original phase - .add(1) // get the original round - it('returns 0', async () => { - const actual = await proxy.getTimestamp(overflowRoundId) - bigNumEquals(0, actual) - }) - }) - }) - - describe('#getTimestamp', () => { - describe('when the relevant round is not available', () => { - beforeEach(async () => { - await proxy.proposeAggregator(flux.address) - await proxy.confirmAggregator(flux.address) - }) - - it('does not revert when called with a non existent ID', async () => { - const proxyId = phaseBase.mul(await proxy.phaseId()).add(1) - const actual = await proxy.getTimestamp(proxyId) - bigNumEquals(0, actual) - }) - }) - - describe('when the relevant info is not available', () => { - it('returns 0', async () => { - const actual = await proxy.getTimestamp(phaseBase.mul(777)) - bigNumEquals(0, actual) - }) - }) - - describe('when the round ID is too large', () => { - const overflowRoundId = BigNumber.from(2) - .pow(255) - .add(phaseBase) // get the original phase - .add(1) // get the original round - - it('returns 0', async () => { - const actual = await proxy.getTimestamp(overflowRoundId) - bigNumEquals(0, actual) - }) - }) - }) - - describe('#latestTimestamp', () => { - beforeEach(async () => { - const height = await aggregator.latestTimestamp() - assert.notEqual('0', height.toString()) - }) - - it('pulls the timestamp from the aggregator', async () => { - bigNumEquals( - await aggregator.latestTimestamp(), - await proxy.latestTimestamp(), - ) - const latestRound = await proxy.latestRound() - bigNumEquals( - await aggregator.latestTimestamp(), - await proxy.getTimestamp(latestRound), - ) - }) - - describe('after being updated to another contract', () => { - beforeEach(async () => { - await increaseTimeBy(30, ethers.provider) - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - - const height2 = await aggregator2.latestTimestamp() - assert.notEqual('0', height2.toString()) - - const height1 = await aggregator.latestTimestamp() - assert.notEqual( - height1.toString(), - height2.toString(), - 'Height1 and Height2 should not be equal', - ) - - await proxy.proposeAggregator(aggregator2.address) - await proxy.confirmAggregator(aggregator2.address) - }) - - it('pulls the timestamp from the new aggregator', async () => { - bigNumEquals( - await aggregator2.latestTimestamp(), - await proxy.latestTimestamp(), - ) - const latestRound = await proxy.latestRound() - bigNumEquals( - await aggregator2.latestTimestamp(), - await proxy.getTimestamp(latestRound), - ) - }) - }) - }) - - describe('#getRoundData', () => { - describe('when pointed at a Historic Aggregator', () => { - beforeEach(async () => { - historicAggregator = await historicAggregatorFactory - .connect(defaultAccount) - .deploy(response2) - await proxy.proposeAggregator(historicAggregator.address) - await proxy.confirmAggregator(historicAggregator.address) - }) - - it('reverts', async () => { - const latestRoundId = await historicAggregator.latestRound() - await evmRevert(proxy.getRoundData(latestRoundId)) - }) - - describe('when pointed at an Aggregator Facade', () => { - beforeEach(async () => { - const facade = await aggregatorFacadeFactory - .connect(defaultAccount) - .deploy(aggregator.address, 18, 'LINK/USD: Aggregator Facade') - await proxy.proposeAggregator(facade.address) - await proxy.confirmAggregator(facade.address) - }) - - it('works for a valid roundId', async () => { - const aggId = await aggregator.latestRound() - const phaseId = phaseBase.mul(await proxy.phaseId()) - const proxyId = phaseId.add(aggId) - - const round = await proxy.getRoundData(proxyId) - bigNumEquals(proxyId, round.id) - bigNumEquals(response, round.answer) - const nowSeconds = new Date().valueOf() / 1000 - assert.isAbove(round.updatedAt.toNumber(), nowSeconds - 120) - bigNumEquals(round.updatedAt, round.startedAt) - bigNumEquals(proxyId, round.answeredInRound) - }) - }) - }) - - describe('when pointed at a FluxAggregator', () => { - beforeEach(async () => { - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - - await proxy.proposeAggregator(aggregator2.address) - await proxy.confirmAggregator(aggregator2.address) - }) - - it('works for a valid round ID', async () => { - const aggId = phaseBase.sub(2) - await aggregator2 - .connect(personas.Carol) - .updateRoundData(aggId, response2, 77, 42) - - const phaseId = phaseBase.mul(await proxy.phaseId()) - const proxyId = phaseId.add(aggId) - - const round = await proxy.getRoundData(proxyId) - bigNumEquals(proxyId, round.id) - bigNumEquals(response2, round.answer) - bigNumEquals(42, round.startedAt) - bigNumEquals(77, round.updatedAt) - bigNumEquals(proxyId, round.answeredInRound) - }) - }) - - it('reads round ID of a previous phase', async () => { - const oldphaseId = phaseBase.mul(await proxy.phaseId()) - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - - await proxy.proposeAggregator(aggregator2.address) - await proxy.confirmAggregator(aggregator2.address) - - const aggId = await aggregator.latestRound() - const proxyId = oldphaseId.add(aggId) - - const round = await proxy.getRoundData(proxyId) - bigNumEquals(proxyId, round.id) - bigNumEquals(response, round.answer) - - const nowSeconds = new Date().valueOf() / 1000 - assert.isAbove(round.startedAt.toNumber(), nowSeconds - 120) - bigNumEquals(round.startedAt, round.updatedAt) - bigNumEquals(proxyId, round.answeredInRound) - }) - }) - - describe('#latestRoundData', () => { - describe('when pointed at a Historic Aggregator', () => { - beforeEach(async () => { - historicAggregator = await historicAggregatorFactory - .connect(defaultAccount) - .deploy(response2) - await proxy.proposeAggregator(historicAggregator.address) - await proxy.confirmAggregator(historicAggregator.address) - }) - - it('reverts', async () => { - await evmRevert(proxy.latestRoundData()) - }) - - describe('when pointed at an Aggregator Facade', () => { - beforeEach(async () => { - const facade = await aggregatorFacadeFactory - .connect(defaultAccount) - .deploy( - historicAggregator.address, - 17, - 'DOGE/ZWL: Aggregator Facade', - ) - await proxy.proposeAggregator(facade.address) - await proxy.confirmAggregator(facade.address) - }) - - it('does not revert', async () => { - const aggId = await historicAggregator.latestRound() - const phaseId = phaseBase.mul(await proxy.phaseId()) - const proxyId = phaseId.add(aggId) - - const round = await proxy.latestRoundData() - bigNumEquals(proxyId, round.id) - bigNumEquals(response2, round.answer) - const nowSeconds = new Date().valueOf() / 1000 - assert.isAbove(round.updatedAt.toNumber(), nowSeconds - 120) - bigNumEquals(round.updatedAt, round.startedAt) - bigNumEquals(proxyId, round.answeredInRound) - }) - - it('uses the decimals set in the constructor', async () => { - bigNumEquals(17, await proxy.decimals()) - }) - - it('uses the description set in the constructor', async () => { - assert.equal('DOGE/ZWL: Aggregator Facade', await proxy.description()) - }) - - it('sets the version to 2', async () => { - bigNumEquals(2, await proxy.version()) - }) - }) - }) - - describe('when pointed at a FluxAggregator', () => { - beforeEach(async () => { - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - - await proxy.proposeAggregator(aggregator2.address) - await proxy.confirmAggregator(aggregator2.address) - }) - - it('does not revert', async () => { - const aggId = phaseBase.sub(2) - await aggregator2 - .connect(personas.Carol) - .updateRoundData(aggId, response2, 77, 42) - - const phaseId = phaseBase.mul(await proxy.phaseId()) - const proxyId = phaseId.add(aggId) - - const round = await proxy.latestRoundData() - bigNumEquals(proxyId, round.id) - bigNumEquals(response2, round.answer) - bigNumEquals(42, round.startedAt) - bigNumEquals(77, round.updatedAt) - bigNumEquals(proxyId, round.answeredInRound) - }) - - it('uses the decimals of the aggregator', async () => { - bigNumEquals(18, await proxy.decimals()) - }) - - it('uses the description of the aggregator', async () => { - assert.equal( - 'v0.6/tests/MockV3Aggregator.sol', - await proxy.description(), - ) - }) - - it('uses the version of the aggregator', async () => { - bigNumEquals(0, await proxy.version()) - }) - }) - }) - - describe('#proposeAggregator', () => { - beforeEach(async () => { - await proxy.transferOwnership(await personas.Carol.getAddress()) - await proxy.connect(personas.Carol).acceptOwnership() - - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, 1) - - assert.equal(aggregator.address, await proxy.aggregator()) - }) - - describe('when called by the owner', () => { - it('sets the address of the proposed aggregator', async () => { - await proxy - .connect(personas.Carol) - .proposeAggregator(aggregator2.address) - - assert.equal(aggregator2.address, await proxy.proposedAggregator()) - }) - - it('emits an AggregatorProposed event', async () => { - const tx = await proxy - .connect(personas.Carol) - .proposeAggregator(aggregator2.address) - const receipt = await tx.wait() - const eventLog = receipt?.events - - assert.equal(eventLog?.length, 1) - assert.equal(eventLog?.[0].event, 'AggregatorProposed') - assert.equal(eventLog?.[0].args?.[0], aggregator.address) - assert.equal(eventLog?.[0].args?.[1], aggregator2.address) - }) - }) - - describe('when called by a non-owner', () => { - it('does not update', async () => { - await evmRevert( - proxy.connect(personas.Neil).proposeAggregator(aggregator2.address), - 'Only callable by owner', - ) - - assert.equal(aggregator.address, await proxy.aggregator()) - }) - }) - }) - - describe('#confirmAggregator', () => { - beforeEach(async () => { - await proxy.transferOwnership(await personas.Carol.getAddress()) - await proxy.connect(personas.Carol).acceptOwnership() - - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, 1) - - assert.equal(aggregator.address, await proxy.aggregator()) - }) - - describe('when called by the owner', () => { - beforeEach(async () => { - await proxy - .connect(personas.Carol) - .proposeAggregator(aggregator2.address) - }) - - it('sets the address of the new aggregator', async () => { - await proxy - .connect(personas.Carol) - .confirmAggregator(aggregator2.address) - - assert.equal(aggregator2.address, await proxy.aggregator()) - }) - - it('increases the phase', async () => { - bigNumEquals(1, await proxy.phaseId()) - - await proxy - .connect(personas.Carol) - .confirmAggregator(aggregator2.address) - - bigNumEquals(2, await proxy.phaseId()) - }) - - it('increases the round ID', async () => { - bigNumEquals(phaseBase.add(1), await proxy.latestRound()) - - await proxy - .connect(personas.Carol) - .confirmAggregator(aggregator2.address) - - bigNumEquals(phaseBase.mul(2).add(1), await proxy.latestRound()) - }) - - it('sets the proxy phase and aggregator', async () => { - assert.equal( - '0x0000000000000000000000000000000000000000', - await proxy.phaseAggregators(2), - ) - - await proxy - .connect(personas.Carol) - .confirmAggregator(aggregator2.address) - - assert.equal(aggregator2.address, await proxy.phaseAggregators(2)) - }) - - it('emits an AggregatorConfirmed event', async () => { - const tx = await proxy - .connect(personas.Carol) - .confirmAggregator(aggregator2.address) - const receipt = await tx.wait() - const eventLog = receipt?.events - - assert.equal(eventLog?.length, 1) - assert.equal(eventLog?.[0].event, 'AggregatorConfirmed') - assert.equal(eventLog?.[0].args?.[0], aggregator.address) - assert.equal(eventLog?.[0].args?.[1], aggregator2.address) - }) - }) - - describe('when called by a non-owner', () => { - beforeEach(async () => { - await proxy - .connect(personas.Carol) - .proposeAggregator(aggregator2.address) - }) - - it('does not update', async () => { - await evmRevert( - proxy.connect(personas.Neil).confirmAggregator(aggregator2.address), - 'Only callable by owner', - ) - - assert.equal(aggregator.address, await proxy.aggregator()) - }) - }) - }) - - describe('#proposedGetRoundData', () => { - beforeEach(async () => { - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - }) - - describe('when an aggregator has been proposed', () => { - beforeEach(async () => { - await proxy - .connect(defaultAccount) - .proposeAggregator(aggregator2.address) - assert.equal(await proxy.proposedAggregator(), aggregator2.address) - }) - - it('returns the data for the proposed aggregator', async () => { - const roundId = await aggregator2.latestRound() - const round = await proxy.proposedGetRoundData(roundId) - bigNumEquals(roundId, round.id) - bigNumEquals(response2, round.answer) - }) - - describe('after the aggregator has been confirmed', () => { - beforeEach(async () => { - await proxy - .connect(defaultAccount) - .confirmAggregator(aggregator2.address) - assert.equal(await proxy.aggregator(), aggregator2.address) - }) - - it('reverts', async () => { - const roundId = await aggregator2.latestRound() - await evmRevert( - proxy.proposedGetRoundData(roundId), - 'No proposed aggregator present', - ) - }) - }) - }) - }) - - describe('#proposedLatestRoundData', () => { - beforeEach(async () => { - aggregator2 = await aggregatorFactory - .connect(defaultAccount) - .deploy(decimals, response2) - }) - - describe('when an aggregator has been proposed', () => { - beforeEach(async () => { - await proxy - .connect(defaultAccount) - .proposeAggregator(aggregator2.address) - assert.equal(await proxy.proposedAggregator(), aggregator2.address) - }) - - it('returns the data for the proposed aggregator', async () => { - const roundId = await aggregator2.latestRound() - const round = await proxy.proposedLatestRoundData() - bigNumEquals(roundId, round.id) - bigNumEquals(response2, round.answer) - }) - - describe('after the aggregator has been confirmed', () => { - beforeEach(async () => { - await proxy - .connect(defaultAccount) - .confirmAggregator(aggregator2.address) - assert.equal(await proxy.aggregator(), aggregator2.address) - }) - - it('reverts', async () => { - await evmRevert( - proxy.proposedLatestRoundData(), - 'No proposed aggregator present', - ) - }) - }) - }) - }) -}) diff --git a/contracts/test/v0.7/AuthorizedForwarder.test.ts b/contracts/test/v0.7/AuthorizedForwarder.test.ts deleted file mode 100644 index e1fa2f1f708..00000000000 --- a/contracts/test/v0.7/AuthorizedForwarder.test.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { Contract, ContractFactory, ContractReceipt } from 'ethers' -import { getUsers, Roles } from '../test-helpers/setup' -import { evmRevert } from '../test-helpers/matchers' - -let getterSetterFactory: ContractFactory -let forwarderFactory: ContractFactory -let brokenFactory: ContractFactory -let linkTokenFactory: ContractFactory - -let roles: Roles -const zeroAddress = ethers.constants.AddressZero - -before(async () => { - const users = await getUsers() - - roles = users.roles - getterSetterFactory = await ethers.getContractFactory( - 'src/v0.4/tests/GetterSetter.sol:GetterSetter', - roles.defaultAccount, - ) - brokenFactory = await ethers.getContractFactory( - 'src/v0.8/tests/Broken.sol:Broken', - roles.defaultAccount, - ) - forwarderFactory = await ethers.getContractFactory( - 'src/v0.7/AuthorizedForwarder.sol:AuthorizedForwarder', - roles.defaultAccount, - ) - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) -}) - -describe('AuthorizedForwarder', () => { - let link: Contract - let forwarder: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - forwarder = await forwarderFactory - .connect(roles.defaultAccount) - .deploy( - link.address, - await roles.defaultAccount.getAddress(), - zeroAddress, - '0x', - ) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(forwarder, [ - 'forward', - 'getAuthorizedSenders', - 'getChainlinkToken', - 'isAuthorizedSender', - 'ownerForward', - 'setAuthorizedSenders', - 'transferOwnershipWithMessage', - 'typeAndVersion', - // ConfirmedOwner - 'transferOwnership', - 'acceptOwnership', - 'owner', - ]) - }) - - describe('#typeAndVersion', () => { - it('describes the authorized forwarder', async () => { - assert.equal( - await forwarder.typeAndVersion(), - 'AuthorizedForwarder 1.0.0', - ) - }) - }) - - describe('deployment', () => { - it('sets the correct link token', async () => { - assert.equal(await forwarder.getChainlinkToken(), link.address) - }) - - it('reverts on zeroAddress value for link token', async () => { - await evmRevert( - forwarderFactory.connect(roles.defaultAccount).deploy( - zeroAddress, // Link Address - await roles.defaultAccount.getAddress(), - zeroAddress, - '0x', - ), - ) - }) - - it('sets no authorized senders', async () => { - const senders = await forwarder.getAuthorizedSenders() - assert.equal(senders.length, 0) - }) - }) - - describe('#setAuthorizedSenders', () => { - let newSenders: string[] - let receipt: ContractReceipt - describe('when called by the owner', () => { - describe('set authorized senders containing duplicate/s', () => { - beforeEach(async () => { - newSenders = [ - await roles.oracleNode1.getAddress(), - await roles.oracleNode1.getAddress(), - await roles.oracleNode2.getAddress(), - await roles.oracleNode3.getAddress(), - ] - }) - it('reverts with a must not have duplicate senders message', async () => { - await evmRevert( - forwarder - .connect(roles.defaultAccount) - .setAuthorizedSenders(newSenders), - 'Must not have duplicate senders', - ) - }) - }) - - describe('setting 3 authorized senders', () => { - beforeEach(async () => { - newSenders = [ - await roles.oracleNode1.getAddress(), - await roles.oracleNode2.getAddress(), - await roles.oracleNode3.getAddress(), - ] - const tx = await forwarder - .connect(roles.defaultAccount) - .setAuthorizedSenders(newSenders) - receipt = await tx.wait() - }) - - it('adds the authorized nodes', async () => { - const authorizedSenders = await forwarder.getAuthorizedSenders() - assert.equal(newSenders.length, authorizedSenders.length) - for (let i = 0; i < authorizedSenders.length; i++) { - assert.equal(authorizedSenders[i], newSenders[i]) - } - }) - - it('emits an event', async () => { - assert.equal(receipt.events?.length, 1) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'AuthorizedSendersChanged') - const encodedSenders = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'address'], - [newSenders, await roles.defaultAccount.getAddress()], - ) - assert.equal(responseEvent?.data, encodedSenders) - }) - - it('replaces the authorized nodes', async () => { - const newSenders = await forwarder - .connect(roles.defaultAccount) - .getAuthorizedSenders() - assert.notIncludeOrderedMembers(newSenders, [ - await roles.oracleNode.getAddress(), - ]) - }) - - after(async () => { - await forwarder - .connect(roles.defaultAccount) - .setAuthorizedSenders([await roles.oracleNode.getAddress()]) - }) - }) - - describe('setting 0 authorized senders', () => { - beforeEach(async () => { - newSenders = [] - }) - - it('reverts with a minimum senders message', async () => { - await evmRevert( - forwarder - .connect(roles.defaultAccount) - .setAuthorizedSenders(newSenders), - 'Must have at least 1 sender', - ) - }) - }) - }) - - describe('when called by a non-owner', () => { - it('cannot add an authorized node', async () => { - await evmRevert( - forwarder - .connect(roles.stranger) - .setAuthorizedSenders([await roles.stranger.getAddress()]), - 'Cannot set authorized senders', - ) - }) - }) - }) - - describe('#forward', () => { - let bytes: string - let payload: string - let mock: Contract - - beforeEach(async () => { - mock = await getterSetterFactory.connect(roles.defaultAccount).deploy() - bytes = ethers.utils.hexlify(ethers.utils.randomBytes(100)) - payload = getterSetterFactory.interface.encodeFunctionData( - getterSetterFactory.interface.getFunction('setBytes'), - [bytes], - ) - }) - - describe('when called by an unauthorized node', () => { - it('reverts', async () => { - await evmRevert( - forwarder.connect(roles.stranger).forward(mock.address, payload), - ) - }) - }) - - describe('when called by an authorized node', () => { - beforeEach(async () => { - await forwarder - .connect(roles.defaultAccount) - .setAuthorizedSenders([await roles.defaultAccount.getAddress()]) - }) - - describe('when destination call reverts', () => { - let brokenMock: Contract - let brokenPayload: string - let brokenMsgPayload: string - - beforeEach(async () => { - brokenMock = await brokenFactory - .connect(roles.defaultAccount) - .deploy() - brokenMsgPayload = brokenFactory.interface.encodeFunctionData( - brokenFactory.interface.getFunction('revertWithMessage'), - ['Failure message'], - ) - - brokenPayload = brokenFactory.interface.encodeFunctionData( - brokenFactory.interface.getFunction('revertSilently'), - [], - ) - }) - - describe('when reverts with message', () => { - it('return revert message', async () => { - await evmRevert( - forwarder - .connect(roles.defaultAccount) - .forward(brokenMock.address, brokenMsgPayload), - "reverted with reason string 'Failure message'", - ) - }) - }) - - describe('when reverts without message', () => { - it('return silent failure message', async () => { - await evmRevert( - forwarder - .connect(roles.defaultAccount) - .forward(brokenMock.address, brokenPayload), - 'Forwarded call reverted without reason', - ) - }) - }) - }) - - describe('when sending to a non-contract address', () => { - it('reverts', async () => { - await evmRevert( - forwarder - .connect(roles.defaultAccount) - .forward(zeroAddress, payload), - 'Must forward to a contract', - ) - }) - }) - - describe('when attempting to forward to the link token', () => { - it('reverts', async () => { - const sighash = linkTokenFactory.interface.getSighash('name') // any Link Token function - await evmRevert( - forwarder - .connect(roles.defaultAccount) - .forward(link.address, sighash), - ) - }) - }) - - describe('when forwarding to any other address', () => { - it('forwards the data', async () => { - const tx = await forwarder - .connect(roles.defaultAccount) - .forward(mock.address, payload) - await tx.wait() - assert.equal(await mock.getBytes(), bytes) - }) - - it('perceives the message is sent by the AuthorizedForwarder', async () => { - const tx = await forwarder - .connect(roles.defaultAccount) - .forward(mock.address, payload) - await expect(tx) - .to.emit(mock, 'SetBytes') - .withArgs(forwarder.address, bytes) - }) - }) - }) - }) - - describe('#transferOwnershipWithMessage', () => { - const message = '0x42' - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - forwarder - .connect(roles.stranger) - .transferOwnershipWithMessage( - await roles.stranger.getAddress(), - message, - ), - 'Only callable by owner', - ) - }) - }) - - describe('when called by the owner', () => { - it('calls the normal ownership transfer proposal', async () => { - const tx = await forwarder - .connect(roles.defaultAccount) - .transferOwnershipWithMessage( - await roles.stranger.getAddress(), - message, - ) - const receipt = await tx.wait() - - assert.equal(receipt?.events?.[0]?.event, 'OwnershipTransferRequested') - assert.equal(receipt?.events?.[0]?.address, forwarder.address) - assert.equal( - receipt?.events?.[0]?.args?.[0], - await roles.defaultAccount.getAddress(), - ) - assert.equal( - receipt?.events?.[0]?.args?.[1], - await roles.stranger.getAddress(), - ) - }) - - it('calls the normal ownership transfer proposal', async () => { - const tx = await forwarder - .connect(roles.defaultAccount) - .transferOwnershipWithMessage( - await roles.stranger.getAddress(), - message, - ) - const receipt = await tx.wait() - - assert.equal( - receipt?.events?.[1]?.event, - 'OwnershipTransferRequestedWithMessage', - ) - assert.equal(receipt?.events?.[1]?.address, forwarder.address) - assert.equal( - receipt?.events?.[1]?.args?.[0], - await roles.defaultAccount.getAddress(), - ) - assert.equal( - receipt?.events?.[1]?.args?.[1], - await roles.stranger.getAddress(), - ) - assert.equal(receipt?.events?.[1]?.args?.[2], message) - }) - }) - }) - - describe('#ownerForward', () => { - let bytes: string - let payload: string - let mock: Contract - - beforeEach(async () => { - mock = await getterSetterFactory.connect(roles.defaultAccount).deploy() - bytes = ethers.utils.hexlify(ethers.utils.randomBytes(100)) - payload = getterSetterFactory.interface.encodeFunctionData( - getterSetterFactory.interface.getFunction('setBytes'), - [bytes], - ) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - forwarder.connect(roles.stranger).ownerForward(mock.address, payload), - ) - }) - }) - - describe('when called by owner', () => { - describe('when attempting to forward to the link token', () => { - it('does not revert', async () => { - const sighash = linkTokenFactory.interface.getSighash('name') // any Link Token function - - await forwarder - .connect(roles.defaultAccount) - .ownerForward(link.address, sighash) - }) - }) - - describe('when forwarding to any other address', () => { - it('forwards the data', async () => { - const tx = await forwarder - .connect(roles.defaultAccount) - .ownerForward(mock.address, payload) - await tx.wait() - assert.equal(await mock.getBytes(), bytes) - }) - - it('reverts when sending to a non-contract address', async () => { - await evmRevert( - forwarder - .connect(roles.defaultAccount) - .ownerForward(zeroAddress, payload), - 'Must forward to a contract', - ) - }) - - it('perceives the message is sent by the Operator', async () => { - const tx = await forwarder - .connect(roles.defaultAccount) - .ownerForward(mock.address, payload) - await expect(tx) - .to.emit(mock, 'SetBytes') - .withArgs(forwarder.address, bytes) - }) - }) - }) - }) -}) diff --git a/contracts/test/v0.7/Chainlink.test.ts b/contracts/test/v0.7/Chainlink.test.ts deleted file mode 100644 index 7792895934c..00000000000 --- a/contracts/test/v0.7/Chainlink.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi, decodeDietCBOR, hexToBuf } from '../test-helpers/helpers' -import { assert } from 'chai' -import { Contract, ContractFactory, providers, Signer } from 'ethers' -import { Roles, getUsers } from '../test-helpers/setup' -import { makeDebug } from '../test-helpers/debug' - -const debug = makeDebug('ChainlinkTestHelper') -let concreteChainlinkFactory: ContractFactory - -let roles: Roles - -before(async () => { - roles = (await getUsers()).roles - concreteChainlinkFactory = await ethers.getContractFactory( - 'src/v0.7/tests/ChainlinkTestHelper.sol:ChainlinkTestHelper', - roles.defaultAccount, - ) -}) - -describe('ChainlinkTestHelper', () => { - let ccl: Contract - let defaultAccount: Signer - - beforeEach(async () => { - defaultAccount = roles.defaultAccount - ccl = await concreteChainlinkFactory.connect(defaultAccount).deploy() - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(ccl, [ - 'add', - 'addBytes', - 'addInt', - 'addStringArray', - 'addUint', - 'closeEvent', - 'setBuffer', - ]) - }) - - async function parseCCLEvent(tx: providers.TransactionResponse) { - const receipt = await tx.wait() - const data = receipt.logs?.[0].data - const d = debug.extend('parseCCLEvent') - d('data %s', data) - return ethers.utils.defaultAbiCoder.decode(['bytes'], data ?? '') - } - - describe('#close', () => { - it('handles empty payloads', async () => { - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, {}) - }) - }) - - describe('#setBuffer', () => { - it('emits the buffer', async () => { - await ccl.setBuffer('0xA161616162') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { a: 'b' }) - }) - }) - - describe('#add', () => { - it('stores and logs keys and values', async () => { - await ccl.add('first', 'word!!') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { first: 'word!!' }) - }) - - it('handles two entries', async () => { - await ccl.add('first', 'uno') - await ccl.add('second', 'dos') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - assert.deepEqual(decoded, { - first: 'uno', - second: 'dos', - }) - }) - }) - - describe('#addBytes', () => { - it('stores and logs keys and values', async () => { - await ccl.addBytes('first', '0xaabbccddeeff') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - const expected = hexToBuf('0xaabbccddeeff') - assert.deepEqual(decoded, { first: expected }) - }) - - it('handles two entries', async () => { - await ccl.addBytes('first', '0x756E6F') - await ccl.addBytes('second', '0x646F73') - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - const expectedFirst = hexToBuf('0x756E6F') - const expectedSecond = hexToBuf('0x646F73') - assert.deepEqual(decoded, { - first: expectedFirst, - second: expectedSecond, - }) - }) - - it('handles strings', async () => { - await ccl.addBytes('first', ethers.utils.toUtf8Bytes('apple')) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - const expected = ethers.utils.toUtf8Bytes('apple') - assert.deepEqual(decoded, { first: expected }) - }) - }) - - describe('#addInt', () => { - it('stores and logs keys and values', async () => { - await ccl.addInt('first', 1) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { first: 1 }) - }) - - it('handles two entries', async () => { - await ccl.addInt('first', 1) - await ccl.addInt('second', 2) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - assert.deepEqual(decoded, { - first: 1, - second: 2, - }) - }) - }) - - describe('#addUint', () => { - it('stores and logs keys and values', async () => { - await ccl.addUint('first', 1) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { first: 1 }) - }) - - it('handles two entries', async () => { - await ccl.addUint('first', 1) - await ccl.addUint('second', 2) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - - assert.deepEqual(decoded, { - first: 1, - second: 2, - }) - }) - }) - - describe('#addStringArray', () => { - it('stores and logs keys and values', async () => { - await ccl.addStringArray('word', [ - ethers.utils.formatBytes32String('seinfeld'), - ethers.utils.formatBytes32String('"4"'), - ethers.utils.formatBytes32String('LIFE'), - ]) - const tx = await ccl.closeEvent() - const [payload] = await parseCCLEvent(tx) - const decoded = await decodeDietCBOR(payload) - assert.deepEqual(decoded, { word: ['seinfeld', '"4"', 'LIFE'] }) - }) - }) -}) diff --git a/contracts/test/v0.7/ChainlinkClient.test.ts b/contracts/test/v0.7/ChainlinkClient.test.ts deleted file mode 100644 index 198d382af79..00000000000 --- a/contracts/test/v0.7/ChainlinkClient.test.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { ethers } from 'hardhat' -import { assert } from 'chai' -import { Contract, ContractFactory } from 'ethers' -import { Roles, getUsers } from '../test-helpers/setup' -import { - convertFufillParams, - decodeCCRequest, - decodeRunRequest, - RunRequest, -} from '../test-helpers/oracle' -import { decodeDietCBOR } from '../test-helpers/helpers' -import { evmRevert } from '../test-helpers/matchers' - -let concreteChainlinkClientFactory: ContractFactory -let emptyOracleFactory: ContractFactory -let getterSetterFactory: ContractFactory -let operatorFactory: ContractFactory -let linkTokenFactory: ContractFactory - -let roles: Roles - -before(async () => { - roles = (await getUsers()).roles - - concreteChainlinkClientFactory = await ethers.getContractFactory( - 'src/v0.7/tests/ChainlinkClientTestHelper.sol:ChainlinkClientTestHelper', - roles.defaultAccount, - ) - emptyOracleFactory = await ethers.getContractFactory( - 'src/v0.6/tests/EmptyOracle.sol:EmptyOracle', - roles.defaultAccount, - ) - getterSetterFactory = await ethers.getContractFactory( - 'src/v0.5/tests/GetterSetter.sol:GetterSetter', - roles.defaultAccount, - ) - operatorFactory = await ethers.getContractFactory( - 'src/v0.7/Operator.sol:Operator', - roles.defaultAccount, - ) - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) -}) - -describe('ChainlinkClientTestHelper', () => { - const specId = - '0x4c7b7ffb66b344fbaa64995af81e355a00000000000000000000000000000000' - let cc: Contract - let gs: Contract - let oc: Contract - let newoc: Contract - let link: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - oc = await operatorFactory - .connect(roles.defaultAccount) - .deploy(link.address, await roles.defaultAccount.getAddress()) - newoc = await operatorFactory - .connect(roles.defaultAccount) - .deploy(link.address, await roles.defaultAccount.getAddress()) - gs = await getterSetterFactory.connect(roles.defaultAccount).deploy() - cc = await concreteChainlinkClientFactory - .connect(roles.defaultAccount) - .deploy(link.address, oc.address) - }) - - describe('#newRequest', () => { - it('forwards the information to the oracle contract through the link token', async () => { - const tx = await cc.publicNewRequest( - specId, - gs.address, - ethers.utils.toUtf8Bytes('requestedBytes32(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - - assert.equal(1, receipt.logs?.length) - const [jId, cbAddr, cbFId, cborData] = receipt.logs - ? decodeCCRequest(receipt.logs[0]) - : [] - const params = decodeDietCBOR(cborData ?? '') - - assert.equal(specId, jId) - assert.equal(gs.address, cbAddr) - assert.equal('0xed53e511', cbFId) - assert.deepEqual({}, params) - }) - }) - - describe('#chainlinkRequest(Request)', () => { - it('emits an event from the contract showing the run ID', async () => { - const tx = await cc.publicRequest( - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - - const { events, logs } = await tx.wait() - - assert.equal(4, events?.length) - - assert.equal(logs?.[0].address, cc.address) - assert.equal(events?.[0].event, 'ChainlinkRequested') - }) - }) - - describe('#chainlinkRequestTo(Request)', () => { - it('emits an event from the contract showing the run ID', async () => { - const tx = await cc.publicRequestRunTo( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { events } = await tx.wait() - - assert.equal(4, events?.length) - assert.equal(events?.[0].event, 'ChainlinkRequested') - }) - - it('emits an event on the target oracle contract', async () => { - const tx = await cc.publicRequestRunTo( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { logs } = await tx.wait() - const event = logs && newoc.interface.parseLog(logs[3]) - - assert.equal(4, logs?.length) - assert.equal(event?.name, 'OracleRequest') - }) - - it('does not modify the stored oracle address', async () => { - await cc.publicRequestRunTo( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - - const actualOracleAddress = await cc.publicOracleAddress() - assert.equal(oc.address, actualOracleAddress) - }) - }) - - describe('#requestOracleData', () => { - it('emits an event from the contract showing the run ID', async () => { - const tx = await cc.publicRequestOracleData( - specId, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - - const { events, logs } = await tx.wait() - - assert.equal(4, events?.length) - - assert.equal(logs?.[0].address, cc.address) - assert.equal(events?.[0].event, 'ChainlinkRequested') - }) - }) - - describe('#requestOracleDataFrom', () => { - it('emits an event from the contract showing the run ID', async () => { - const tx = await cc.publicRequestOracleDataFrom( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { events } = await tx.wait() - - assert.equal(4, events?.length) - assert.equal(events?.[0].event, 'ChainlinkRequested') - }) - - it('emits an event on the target oracle contract', async () => { - const tx = await cc.publicRequestOracleDataFrom( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { logs } = await tx.wait() - const event = logs && newoc.interface.parseLog(logs[3]) - - assert.equal(4, logs?.length) - assert.equal(event?.name, 'OracleRequest') - }) - - it('does not modify the stored oracle address', async () => { - await cc.publicRequestOracleDataFrom( - newoc.address, - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - - const actualOracleAddress = await cc.publicOracleAddress() - assert.equal(oc.address, actualOracleAddress) - }) - }) - - describe('#cancelChainlinkRequest', () => { - let requestId: string - // a concrete chainlink attached to an empty oracle - let ecc: Contract - - beforeEach(async () => { - const emptyOracle = await emptyOracleFactory - .connect(roles.defaultAccount) - .deploy() - ecc = await concreteChainlinkClientFactory - .connect(roles.defaultAccount) - .deploy(link.address, emptyOracle.address) - - const tx = await ecc.publicRequest( - specId, - ecc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { events } = await tx.wait() - requestId = (events?.[0]?.args as any).id - }) - - it('emits an event from the contract showing the run was cancelled', async () => { - const tx = await ecc.publicCancelRequest( - requestId, - 0, - ethers.utils.hexZeroPad('0x', 4), - 0, - ) - const { events } = await tx.wait() - - assert.equal(1, events?.length) - assert.equal(events?.[0].event, 'ChainlinkCancelled') - assert.equal(requestId, (events?.[0].args as any).id) - }) - - it('throws if given a bogus event ID', async () => { - await evmRevert( - ecc.publicCancelRequest( - ethers.utils.formatBytes32String('bogusId'), - 0, - ethers.utils.hexZeroPad('0x', 4), - 0, - ), - ) - }) - }) - - describe('#recordChainlinkFulfillment(modifier)', () => { - let request: RunRequest - - beforeEach(async () => { - await oc.setAuthorizedSenders([await roles.defaultAccount.getAddress()]) - const tx = await cc.publicRequest( - specId, - cc.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const { logs } = await tx.wait() - - request = decodeRunRequest(logs?.[3]) - }) - - it('emits an event marking the request fulfilled', async () => { - const tx = await oc - .connect(roles.defaultAccount) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - const { logs } = await tx.wait() - - const event = logs && cc.interface.parseLog(logs[1]) - - assert.equal(2, logs?.length) - assert.equal(event?.name, 'ChainlinkFulfilled') - assert.equal(request.requestId, event?.args.id) - }) - - it('should only allow one fulfillment per id', async () => { - await oc - .connect(roles.defaultAccount) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - - await evmRevert( - oc - .connect(roles.defaultAccount) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Must have a valid requestId', - ) - }) - - it('should only allow the oracle to fulfill the request', async () => { - await evmRevert( - oc - .connect(roles.stranger) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Not authorized sender', - ) - }) - }) - - describe('#fulfillChainlinkRequest(function)', () => { - let request: RunRequest - - beforeEach(async () => { - await oc.setAuthorizedSenders([await roles.defaultAccount.getAddress()]) - const tx = await cc.publicRequest( - specId, - cc.address, - ethers.utils.toUtf8Bytes( - 'publicFulfillChainlinkRequest(bytes32,bytes32)', - ), - 0, - ) - const { logs } = await tx.wait() - - request = decodeRunRequest(logs?.[3]) - }) - - it('emits an event marking the request fulfilled', async () => { - const tx = await oc - .connect(roles.defaultAccount) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - - const { logs } = await tx.wait() - const event = logs && cc.interface.parseLog(logs[1]) - - assert.equal(2, logs?.length) - assert.equal(event?.name, 'ChainlinkFulfilled') - assert.equal(request.requestId, event?.args?.id) - }) - - it('should only allow one fulfillment per id', async () => { - await oc - .connect(roles.defaultAccount) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - - await evmRevert( - oc - .connect(roles.defaultAccount) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Must have a valid requestId', - ) - }) - - it('should only allow the oracle to fulfill the request', async () => { - await evmRevert( - oc - .connect(roles.stranger) - .fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ), - 'Not authorized sender', - ) - }) - }) - - describe('#chainlinkToken', () => { - it('returns the Link Token address', async () => { - const addr = await cc.publicChainlinkToken() - assert.equal(addr, link.address) - }) - }) - - describe('#addExternalRequest', () => { - let mock: Contract - let request: RunRequest - - beforeEach(async () => { - mock = await concreteChainlinkClientFactory - .connect(roles.defaultAccount) - .deploy(link.address, oc.address) - - const tx = await cc.publicRequest( - specId, - mock.address, - ethers.utils.toUtf8Bytes('fulfillRequest(bytes32,bytes32)'), - 0, - ) - const receipt = await tx.wait() - - request = decodeRunRequest(receipt.logs?.[3]) - await mock.publicAddExternalRequest(oc.address, request.requestId) - }) - - it('allows the external request to be fulfilled', async () => { - await oc.setAuthorizedSenders([await roles.defaultAccount.getAddress()]) - await oc.fulfillOracleRequest( - ...convertFufillParams( - request, - ethers.utils.formatBytes32String('hi mom!'), - ), - ) - }) - - it('does not allow the same requestId to be used', async () => { - await evmRevert( - cc.publicAddExternalRequest(newoc.address, request.requestId), - ) - }) - }) -}) diff --git a/contracts/test/v0.7/CompoundPriceFlaggingValidator.test.ts b/contracts/test/v0.7/CompoundPriceFlaggingValidator.test.ts deleted file mode 100644 index 315f7bd9e6b..00000000000 --- a/contracts/test/v0.7/CompoundPriceFlaggingValidator.test.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { ethers } from 'hardhat' -import { evmWordToAddress, getLogs, publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { - BigNumber, - Contract, - ContractFactory, - ContractTransaction, -} from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { evmRevert } from '../test-helpers/matchers' - -let personas: Personas -let validatorFactory: ContractFactory -let acFactory: ContractFactory -let flagsFactory: ContractFactory -let aggregatorFactory: ContractFactory -let compoundOracleFactory: ContractFactory - -before(async () => { - personas = (await getUsers()).personas - - validatorFactory = await ethers.getContractFactory( - 'src/v0.7/dev/CompoundPriceFlaggingValidator.sol:CompoundPriceFlaggingValidator', - personas.Carol, - ) - acFactory = await ethers.getContractFactory( - 'src/v0.6/SimpleWriteAccessController.sol:SimpleWriteAccessController', - personas.Carol, - ) - flagsFactory = await ethers.getContractFactory( - 'src/v0.6/Flags.sol:Flags', - personas.Carol, - ) - aggregatorFactory = await ethers.getContractFactory( - 'src/v0.7/tests/MockV3Aggregator.sol:MockV3Aggregator', - personas.Carol, - ) - compoundOracleFactory = await ethers.getContractFactory( - 'src/v0.7/tests/MockCompoundOracle.sol:MockCompoundOracle', - personas.Carol, - ) -}) - -describe('CompoundPriceFlaggingVlidator', () => { - let validator: Contract - let aggregator: Contract - let compoundOracle: Contract - let flags: Contract - let ac: Contract - - const aggregatorDecimals = 18 - // 1000 - const initialAggregatorPrice = BigNumber.from('1000000000000000000000') - - const compoundSymbol = 'ETH' - const compoundDecimals = 6 - // 1100 (10% deviation from aggregator price) - const initialCompoundPrice = BigNumber.from('1100000000') - - // (50,000,000 / 1,000,000,000) = 0.05 = 5% deviation threshold - const initialDeviationNumerator = 50_000_000 - - beforeEach(async () => { - ac = await acFactory.connect(personas.Carol).deploy() - flags = await flagsFactory.connect(personas.Carol).deploy(ac.address) - aggregator = await aggregatorFactory - .connect(personas.Carol) - .deploy(aggregatorDecimals, initialAggregatorPrice) - compoundOracle = await compoundOracleFactory - .connect(personas.Carol) - .deploy() - await compoundOracle.setPrice( - compoundSymbol, - initialCompoundPrice, - compoundDecimals, - ) - validator = await validatorFactory - .connect(personas.Carol) - .deploy(flags.address, compoundOracle.address) - await validator - .connect(personas.Carol) - .setFeedDetails( - aggregator.address, - compoundSymbol, - compoundDecimals, - initialDeviationNumerator, - ) - await ac.connect(personas.Carol).addAccess(validator.address) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(validator, [ - 'update', - 'check', - 'setFeedDetails', - 'setFlagsAddress', - 'setCompoundOpenOracleAddress', - 'getFeedDetails', - 'flags', - 'compoundOpenOracle', - // Upkeep methods: - 'checkUpkeep', - 'performUpkeep', - // Owned methods: - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('#constructor', () => { - it('sets the owner', async () => { - assert.equal(await validator.owner(), await personas.Carol.getAddress()) - }) - - it('sets the arguments passed in', async () => { - assert.equal(await validator.flags(), flags.address) - assert.equal(await validator.compoundOpenOracle(), compoundOracle.address) - }) - }) - - describe('#setOpenOracleAddress', () => { - let newCompoundOracle: Contract - let tx: ContractTransaction - - beforeEach(async () => { - newCompoundOracle = await compoundOracleFactory - .connect(personas.Carol) - .deploy() - tx = await validator - .connect(personas.Carol) - .setCompoundOpenOracleAddress(newCompoundOracle.address) - }) - - it('changes the compound oracke address', async () => { - assert.equal( - await validator.compoundOpenOracle(), - newCompoundOracle.address, - ) - }) - - it('emits a log event', async () => { - await expect(tx) - .to.emit(validator, 'CompoundOpenOracleAddressUpdated') - .withArgs(compoundOracle.address, newCompoundOracle.address) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - validator - .connect(personas.Neil) - .setCompoundOpenOracleAddress(newCompoundOracle.address), - 'Only callable by owner', - ) - }) - }) - }) - - describe('#setFlagsAddress', () => { - let newFlagsContract: Contract - let tx: ContractTransaction - - beforeEach(async () => { - newFlagsContract = await flagsFactory - .connect(personas.Carol) - .deploy(ac.address) - tx = await validator - .connect(personas.Carol) - .setFlagsAddress(newFlagsContract.address) - }) - - it('changes the flags address', async () => { - assert.equal(await validator.flags(), newFlagsContract.address) - }) - - it('emits a log event', async () => { - await expect(tx) - .to.emit(validator, 'FlagsAddressUpdated') - .withArgs(flags.address, newFlagsContract.address) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - validator - .connect(personas.Neil) - .setFlagsAddress(newFlagsContract.address), - 'Only callable by owner', - ) - }) - }) - }) - - describe('#setFeedDetails', () => { - let mockAggregator: Contract - let tx: ContractTransaction - const symbol = 'BTC' - const decimals = 8 - const deviationNumerator = 50_000_000 // 5% - - beforeEach(async () => { - await compoundOracle.connect(personas.Carol).setPrice('BTC', 1500000, 2) - mockAggregator = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, 4000000000000) - tx = await validator - .connect(personas.Carol) - .setFeedDetails( - mockAggregator.address, - symbol, - decimals, - deviationNumerator, - ) - }) - - it('sets the correct state', async () => { - const response = await validator - .connect(personas.Carol) - .getFeedDetails(mockAggregator.address) - - assert.equal(response[0], symbol) - assert.equal(response[1], decimals) - assert.equal(response[2].toString(), deviationNumerator.toString()) - }) - - it('uses the existing symbol if one already exists', async () => { - const newSymbol = 'LINK' - - await compoundOracle - .connect(personas.Carol) - .setPrice(newSymbol, 1500000, 2) - - tx = await validator - .connect(personas.Carol) - .setFeedDetails( - mockAggregator.address, - newSymbol, - decimals, - deviationNumerator, - ) - - // Check the event - await expect(tx) - .to.emit(validator, 'FeedDetailsSet') - .withArgs(mockAggregator.address, symbol, decimals, deviationNumerator) - - // Check the state - const response = await validator - .connect(personas.Carol) - .getFeedDetails(mockAggregator.address) - assert.equal(response[0], symbol) - }) - - it('emits an event', async () => { - await expect(tx) - .to.emit(validator, 'FeedDetailsSet') - .withArgs(mockAggregator.address, symbol, decimals, deviationNumerator) - }) - - it('fails when given a 0 numerator', async () => { - await evmRevert( - validator - .connect(personas.Carol) - .setFeedDetails(mockAggregator.address, symbol, decimals, 0), - 'Invalid threshold numerator', - ) - }) - - it('fails when given a numerator above 1 billion', async () => { - await evmRevert( - validator - .connect(personas.Carol) - .setFeedDetails( - mockAggregator.address, - symbol, - decimals, - 1_200_000_000, - ), - 'Invalid threshold numerator', - ) - }) - - it('fails when the compound price is invalid', async () => { - await evmRevert( - validator - .connect(personas.Carol) - .setFeedDetails( - mockAggregator.address, - 'TEST', - decimals, - deviationNumerator, - ), - 'Invalid Compound price', - ) - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - validator - .connect(personas.Neil) - .setFeedDetails( - mockAggregator.address, - symbol, - decimals, - deviationNumerator, - ), - 'Only callable by owner', - ) - }) - }) - }) - - describe('#check', () => { - describe('with a single aggregator', () => { - describe('with a deviated price exceding threshold', () => { - it('returns the deviated aggregator', async () => { - const aggregators = [aggregator.address] - const response = await validator.check(aggregators) - assert.equal(response.length, 1) - assert.equal(response[0], aggregator.address) - }) - }) - - describe('with a price within the threshold', () => { - const newCompoundPrice = BigNumber.from('1000000000') - beforeEach(async () => { - await compoundOracle.setPrice( - 'ETH', - newCompoundPrice, - compoundDecimals, - ) - }) - - it('returns an empty array', async () => { - const aggregators = [aggregator.address] - const response = await validator.check(aggregators) - assert.equal(response.length, 0) - }) - }) - }) - }) - - describe('#update', () => { - describe('with a single aggregator', () => { - describe('with a deviated price exceding threshold', () => { - it('raises a flag on the flags contract', async () => { - const aggregators = [aggregator.address] - const tx = await validator.connect(personas.Carol).update(aggregators) - const logs = await getLogs(tx) - assert.equal(logs.length, 1) - assert.equal(evmWordToAddress(logs[0].topics[1]), aggregator.address) - }) - }) - - describe('with a price within the threshold', () => { - const newCompoundPrice = BigNumber.from('1000000000') - beforeEach(async () => { - await compoundOracle.setPrice( - 'ETH', - newCompoundPrice, - compoundDecimals, - ) - }) - - it('does nothing', async () => { - const aggregators = [aggregator.address] - const tx = await validator.connect(personas.Carol).update(aggregators) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - }) - }) - - describe('#checkUpkeep', () => { - describe('with a single aggregator', () => { - describe('with a deviated price exceding threshold', () => { - it('returns the deviated aggregator', async () => { - const aggregators = [aggregator.address] - const encodedAggregators = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const response = await validator - .connect(personas.Carol) - .checkUpkeep(encodedAggregators) - - const decodedResponse = ethers.utils.defaultAbiCoder.decode( - ['address[]'], - response?.[1], - ) - assert.equal(decodedResponse?.[0]?.[0], aggregators[0]) - }) - }) - - describe('with a price within the threshold', () => { - const newCompoundPrice = BigNumber.from('1000000000') - beforeEach(async () => { - await compoundOracle.setPrice( - 'ETH', - newCompoundPrice, - compoundDecimals, - ) - }) - - it('returns an empty array', async () => { - const aggregators = [aggregator.address] - const encodedAggregators = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const response = await validator - .connect(personas.Carol) - .checkUpkeep(encodedAggregators) - const decodedResponse = ethers.utils.defaultAbiCoder.decode( - ['address[]'], - response?.[1], - ) - assert.equal(decodedResponse?.[0]?.length, 0) - }) - }) - }) - }) - - describe('#performUpkeep', () => { - describe('with a single aggregator', () => { - describe('with a deviated price exceding threshold', () => { - it('raises a flag on the flags contract', async () => { - const aggregators = [aggregator.address] - const encodedAggregators = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const tx = await validator - .connect(personas.Carol) - .performUpkeep(encodedAggregators) - const logs = await getLogs(tx) - assert.equal(logs.length, 1) - assert.equal(evmWordToAddress(logs[0].topics[1]), aggregator.address) - }) - }) - - describe('with a price within the threshold', () => { - const newCompoundPrice = BigNumber.from('1000000000') - beforeEach(async () => { - await compoundOracle.setPrice( - 'ETH', - newCompoundPrice, - compoundDecimals, - ) - }) - - it('does nothing', async () => { - const aggregators = [aggregator.address] - const encodedAggregators = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const tx = await validator - .connect(personas.Carol) - .performUpkeep(encodedAggregators) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - }) - }) -}) diff --git a/contracts/test/v0.7/ConfirmedOwner.test.ts b/contracts/test/v0.7/ConfirmedOwner.test.ts deleted file mode 100644 index 3502cd15bc2..00000000000 --- a/contracts/test/v0.7/ConfirmedOwner.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { ethers } from 'hardhat' -import { publicAbi } from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { Contract, ContractFactory, Signer } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { evmRevert } from '../test-helpers/matchers' - -let confirmedOwnerTestHelperFactory: ContractFactory -let confirmedOwnerFactory: ContractFactory - -let personas: Personas -let owner: Signer -let nonOwner: Signer -let newOwner: Signer - -before(async () => { - const users = await getUsers() - personas = users.personas - owner = personas.Carol - nonOwner = personas.Neil - newOwner = personas.Ned - - confirmedOwnerTestHelperFactory = await ethers.getContractFactory( - 'src/v0.7/tests/ConfirmedOwnerTestHelper.sol:ConfirmedOwnerTestHelper', - owner, - ) - confirmedOwnerFactory = await ethers.getContractFactory( - 'src/v0.7/ConfirmedOwner.sol:ConfirmedOwner', - owner, - ) -}) - -describe('ConfirmedOwner', () => { - let confirmedOwner: Contract - - beforeEach(async () => { - confirmedOwner = await confirmedOwnerTestHelperFactory - .connect(owner) - .deploy() - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(confirmedOwner, [ - 'acceptOwnership', - 'owner', - 'transferOwnership', - // test helper public methods - 'modifierOnlyOwner', - ]) - }) - - describe('#constructor', () => { - it('assigns ownership to the deployer', async () => { - const [actual, expected] = await Promise.all([ - owner.getAddress(), - confirmedOwner.owner(), - ]) - - assert.equal(actual, expected) - }) - - it('reverts if assigned to the zero address', async () => { - await evmRevert( - confirmedOwnerFactory - .connect(owner) - .deploy(ethers.constants.AddressZero), - 'Cannot set owner to zero', - ) - }) - }) - - describe('#onlyOwner modifier', () => { - describe('when called by an owner', () => { - it('successfully calls the method', async () => { - const tx = await confirmedOwner.connect(owner).modifierOnlyOwner() - await expect(tx).to.emit(confirmedOwner, 'Here') - }) - }) - - describe('when called by anyone but the owner', () => { - it('reverts', async () => - await evmRevert(confirmedOwner.connect(nonOwner).modifierOnlyOwner())) - }) - }) - - describe('#transferOwnership', () => { - describe('when called by an owner', () => { - it('emits a log', async () => { - const tx = await confirmedOwner - .connect(owner) - .transferOwnership(await newOwner.getAddress()) - await expect(tx) - .to.emit(confirmedOwner, 'OwnershipTransferRequested') - .withArgs(await owner.getAddress(), await newOwner.getAddress()) - }) - - it('does not allow ownership transfer to self', async () => { - await evmRevert( - confirmedOwner - .connect(owner) - .transferOwnership(await owner.getAddress()), - 'Cannot transfer to self', - ) - }) - }) - }) - - describe('when called by anyone but the owner', () => { - it('reverts', async () => - await evmRevert( - confirmedOwner - .connect(nonOwner) - .transferOwnership(await newOwner.getAddress()), - )) - }) - - describe('#acceptOwnership', () => { - describe('after #transferOwnership has been called', () => { - beforeEach(async () => { - await confirmedOwner - .connect(owner) - .transferOwnership(await newOwner.getAddress()) - }) - - it('allows the recipient to call it', async () => { - const tx = await confirmedOwner.connect(newOwner).acceptOwnership() - await expect(tx) - .to.emit(confirmedOwner, 'OwnershipTransferred') - .withArgs(await owner.getAddress(), await newOwner.getAddress()) - }) - - it('does not allow a non-recipient to call it', async () => - await evmRevert(confirmedOwner.connect(nonOwner).acceptOwnership())) - }) - }) -}) diff --git a/contracts/test/v0.7/KeeperRegistry1_1.test.ts b/contracts/test/v0.7/KeeperRegistry1_1.test.ts deleted file mode 100644 index 4e3a8c91b35..00000000000 --- a/contracts/test/v0.7/KeeperRegistry1_1.test.ts +++ /dev/null @@ -1,1725 +0,0 @@ -import { ethers } from 'hardhat' -import { assert, expect } from 'chai' -import { evmRevert } from '../test-helpers/matchers' -import { getUsers, Personas } from '../test-helpers/setup' -import { BigNumber, BigNumberish, Signer } from 'ethers' -import { LinkToken__factory as LinkTokenFactory } from '../../typechain/factories/LinkToken__factory' -import { KeeperRegistry1_1__factory as KeeperRegistryFactory } from '../../typechain/factories/KeeperRegistry1_1__factory' -import { MockV3Aggregator__factory as MockV3AggregatorFactory } from '../../typechain/factories/MockV3Aggregator__factory' -import { UpkeepMock__factory as UpkeepMockFactory } from '../../typechain/factories/UpkeepMock__factory' -import { UpkeepReverter__factory as UpkeepReverterFactory } from '../../typechain/factories/UpkeepReverter__factory' -import { KeeperRegistry1_1 as KeeperRegistry } from '../../typechain/KeeperRegistry1_1' -import { MockV3Aggregator } from '../../typechain/MockV3Aggregator' -import { LinkToken } from '../../typechain/LinkToken' -import { UpkeepMock } from '../../typechain/UpkeepMock' -import { toWei } from '../test-helpers/helpers' - -async function getUpkeepID(tx: any) { - const receipt = await tx.wait() - return receipt.events[0].args.id -} - -// ----------------------------------------------------------------------------------------------- -// DEV: these *should* match the perform/check gas overhead values in the contract and on the node -const PERFORM_GAS_OVERHEAD = BigNumber.from(90000) -const CHECK_GAS_OVERHEAD = BigNumber.from(170000) -// ----------------------------------------------------------------------------------------------- - -// Smart contract factories -let linkTokenFactory: LinkTokenFactory -let mockV3AggregatorFactory: MockV3AggregatorFactory -let keeperRegistryFactory: KeeperRegistryFactory -let upkeepMockFactory: UpkeepMockFactory -let upkeepReverterFactory: UpkeepReverterFactory - -let personas: Personas - -before(async () => { - personas = (await getUsers()).personas - - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - ) - // need full path because there are two contracts with name MockV3Aggregator - mockV3AggregatorFactory = (await ethers.getContractFactory( - 'src/v0.7/tests/MockV3Aggregator.sol:MockV3Aggregator', - )) as unknown as MockV3AggregatorFactory - // @ts-ignore bug in autogen file - keeperRegistryFactory = await ethers.getContractFactory('KeeperRegistry1_1') - upkeepMockFactory = await ethers.getContractFactory('UpkeepMock') - upkeepReverterFactory = await ethers.getContractFactory('UpkeepReverter') -}) - -describe('KeeperRegistry1_1', () => { - const linkEth = BigNumber.from(300000000) - const gasWei = BigNumber.from(100) - const linkDivisibility = BigNumber.from('1000000000000000000') - const executeGas = BigNumber.from('100000') - const paymentPremiumBase = BigNumber.from('1000000000') - const paymentPremiumPPB = BigNumber.from('250000000') - const flatFeeMicroLink = BigNumber.from(0) - const blockCountPerTurn = BigNumber.from(3) - const emptyBytes = '0x00' - const zeroAddress = ethers.constants.AddressZero - const extraGas = BigNumber.from('250000') - const registryGasOverhead = BigNumber.from('80000') - const stalenessSeconds = BigNumber.from(43820) - const gasCeilingMultiplier = BigNumber.from(1) - const maxCheckGas = BigNumber.from(20000000) - const fallbackGasPrice = BigNumber.from(200) - const fallbackLinkPrice = BigNumber.from(200000000) - - let owner: Signer - let keeper1: Signer - let keeper2: Signer - let keeper3: Signer - let nonkeeper: Signer - let admin: Signer - let payee1: Signer - let payee2: Signer - let payee3: Signer - - let linkToken: LinkToken - let linkEthFeed: MockV3Aggregator - let gasPriceFeed: MockV3Aggregator - let registry: KeeperRegistry - let mock: UpkeepMock - - let id: BigNumber - let keepers: string[] - let payees: string[] - - beforeEach(async () => { - owner = personas.Default - keeper1 = personas.Carol - keeper2 = personas.Eddy - keeper3 = personas.Nancy - nonkeeper = personas.Ned - admin = personas.Neil - payee1 = personas.Nelly - payee2 = personas.Norbert - payee3 = personas.Nick - - keepers = [ - await keeper1.getAddress(), - await keeper2.getAddress(), - await keeper3.getAddress(), - ] - payees = [ - await payee1.getAddress(), - await payee2.getAddress(), - await payee3.getAddress(), - ] - - linkToken = await linkTokenFactory.connect(owner).deploy() - gasPriceFeed = await mockV3AggregatorFactory - .connect(owner) - .deploy(0, gasWei) - linkEthFeed = await mockV3AggregatorFactory - .connect(owner) - .deploy(9, linkEth) - registry = await keeperRegistryFactory - .connect(owner) - .deploy( - linkToken.address, - linkEthFeed.address, - gasPriceFeed.address, - paymentPremiumPPB, - flatFeeMicroLink, - blockCountPerTurn, - maxCheckGas, - stalenessSeconds, - gasCeilingMultiplier, - fallbackGasPrice, - fallbackLinkPrice, - ) - - mock = await upkeepMockFactory.deploy() - await linkToken - .connect(owner) - .transfer(await keeper1.getAddress(), toWei('1000')) - await linkToken - .connect(owner) - .transfer(await keeper2.getAddress(), toWei('1000')) - await linkToken - .connect(owner) - .transfer(await keeper3.getAddress(), toWei('1000')) - - await registry.connect(owner).setKeepers(keepers, payees) - const tx = await registry - .connect(owner) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ) - id = await getUpkeepID(tx) - }) - - const linkForGas = ( - upkeepGasSpent: BigNumberish, - premiumPPB?: BigNumberish, - flatFee?: BigNumberish, - ) => { - premiumPPB = premiumPPB === undefined ? paymentPremiumPPB : premiumPPB - flatFee = flatFee === undefined ? flatFeeMicroLink : flatFee - const gasSpent = registryGasOverhead.add(BigNumber.from(upkeepGasSpent)) - const base = gasWei.mul(gasSpent).mul(linkDivisibility).div(linkEth) - const premium = base.mul(premiumPPB).div(paymentPremiumBase) - const flatFeeJules = BigNumber.from(flatFee).mul('1000000000000') - return base.add(premium).add(flatFeeJules) - } - - describe('#setKeepers', () => { - const IGNORE_ADDRESS = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF' - it('reverts when not called by the owner', async () => { - await evmRevert( - registry.connect(keeper1).setKeepers([], []), - 'Only callable by owner', - ) - }) - - it('reverts when adding the same keeper twice', async () => { - await evmRevert( - registry - .connect(owner) - .setKeepers( - [await keeper1.getAddress(), await keeper1.getAddress()], - [await payee1.getAddress(), await payee1.getAddress()], - ), - 'cannot add keeper twice', - ) - }) - - it('reverts with different numbers of keepers/payees', async () => { - await evmRevert( - registry - .connect(owner) - .setKeepers( - [await keeper1.getAddress(), await keeper2.getAddress()], - [await payee1.getAddress()], - ), - 'address lists not the same length', - ) - await evmRevert( - registry - .connect(owner) - .setKeepers( - [await keeper1.getAddress()], - [await payee1.getAddress(), await payee2.getAddress()], - ), - 'address lists not the same length', - ) - }) - - it('reverts if the payee is the zero address', async () => { - await evmRevert( - registry - .connect(owner) - .setKeepers( - [await keeper1.getAddress(), await keeper2.getAddress()], - [ - await payee1.getAddress(), - '0x0000000000000000000000000000000000000000', - ], - ), - 'cannot set payee to the zero address', - ) - }) - - it('emits events for every keeper added and removed', async () => { - const oldKeepers = [ - await keeper1.getAddress(), - await keeper2.getAddress(), - ] - const oldPayees = [await payee1.getAddress(), await payee2.getAddress()] - await registry.connect(owner).setKeepers(oldKeepers, oldPayees) - assert.deepEqual(oldKeepers, await registry.getKeeperList()) - - // remove keepers - const newKeepers = [ - await keeper2.getAddress(), - await keeper3.getAddress(), - ] - const newPayees = [await payee2.getAddress(), await payee3.getAddress()] - const tx = await registry.connect(owner).setKeepers(newKeepers, newPayees) - assert.deepEqual(newKeepers, await registry.getKeeperList()) - - await expect(tx) - .to.emit(registry, 'KeepersUpdated') - .withArgs(newKeepers, newPayees) - }) - - it('updates the keeper to inactive when removed', async () => { - await registry.connect(owner).setKeepers(keepers, payees) - await registry - .connect(owner) - .setKeepers( - [await keeper1.getAddress(), await keeper3.getAddress()], - [await payee1.getAddress(), await payee3.getAddress()], - ) - const added = await registry.getKeeperInfo(await keeper1.getAddress()) - assert.isTrue(added.active) - const removed = await registry.getKeeperInfo(await keeper2.getAddress()) - assert.isFalse(removed.active) - }) - - it('does not change the payee if IGNORE_ADDRESS is used as payee', async () => { - const oldKeepers = [ - await keeper1.getAddress(), - await keeper2.getAddress(), - ] - const oldPayees = [await payee1.getAddress(), await payee2.getAddress()] - await registry.connect(owner).setKeepers(oldKeepers, oldPayees) - assert.deepEqual(oldKeepers, await registry.getKeeperList()) - - const newKeepers = [ - await keeper2.getAddress(), - await keeper3.getAddress(), - ] - const newPayees = [IGNORE_ADDRESS, await payee3.getAddress()] - const tx = await registry.connect(owner).setKeepers(newKeepers, newPayees) - assert.deepEqual(newKeepers, await registry.getKeeperList()) - - const ignored = await registry.getKeeperInfo(await keeper2.getAddress()) - assert.equal(await payee2.getAddress(), ignored.payee) - assert.equal(true, ignored.active) - - await expect(tx) - .to.emit(registry, 'KeepersUpdated') - .withArgs(newKeepers, newPayees) - }) - - it('reverts if the owner changes the payee', async () => { - await registry.connect(owner).setKeepers(keepers, payees) - await evmRevert( - registry - .connect(owner) - .setKeepers(keepers, [ - await payee1.getAddress(), - await payee2.getAddress(), - await owner.getAddress(), - ]), - 'cannot change payee', - ) - }) - }) - - describe('#registerUpkeep', () => { - it('reverts if the target is not a contract', async () => { - await evmRevert( - registry - .connect(owner) - .registerUpkeep( - zeroAddress, - executeGas, - await admin.getAddress(), - emptyBytes, - ), - 'target is not a contract', - ) - }) - - it('reverts if called by a non-owner', async () => { - await evmRevert( - registry - .connect(keeper1) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ), - 'Only callable by owner or registrar', - ) - }) - - it('reverts if execute gas is too low', async () => { - await evmRevert( - registry - .connect(owner) - .registerUpkeep( - mock.address, - 2299, - await admin.getAddress(), - emptyBytes, - ), - 'min gas is 2300', - ) - }) - - it('reverts if execute gas is too high', async () => { - await evmRevert( - registry - .connect(owner) - .registerUpkeep( - mock.address, - 5000001, - await admin.getAddress(), - emptyBytes, - ), - 'max gas is 5000000', - ) - }) - - it('creates a record of the registration', async () => { - const tx = await registry - .connect(owner) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ) - id = await getUpkeepID(tx) - await expect(tx) - .to.emit(registry, 'UpkeepRegistered') - .withArgs(id, executeGas, await admin.getAddress()) - const registration = await registry.getUpkeep(id) - assert.equal(mock.address, registration.target) - assert.equal(0, registration.balance.toNumber()) - assert.equal(emptyBytes, registration.checkData) - assert(registration.maxValidBlocknumber.eq('0xffffffffffffffff')) - }) - }) - - describe('#addFunds', () => { - const amount = toWei('1') - - beforeEach(async () => { - await linkToken.connect(keeper1).approve(registry.address, toWei('100')) - }) - - it('reverts if the registration does not exist', async () => { - await evmRevert( - registry.connect(keeper1).addFunds(id.add(1), amount), - 'upkeep must be active', - ) - }) - - it('adds to the balance of the registration', async () => { - await registry.connect(keeper1).addFunds(id, amount) - const registration = await registry.getUpkeep(id) - assert.isTrue(amount.eq(registration.balance)) - }) - - it('emits a log', async () => { - const tx = await registry.connect(keeper1).addFunds(id, amount) - await expect(tx) - .to.emit(registry, 'FundsAdded') - .withArgs(id, await keeper1.getAddress(), amount) - }) - - it('reverts if the upkeep is canceled', async () => { - await registry.connect(admin).cancelUpkeep(id) - await evmRevert( - registry.connect(keeper1).addFunds(id, amount), - 'upkeep must be active', - ) - }) - }) - - describe('#checkUpkeep', () => { - it('reverts if the upkeep is not funded', async () => { - await mock.setCanPerform(true) - await mock.setCanCheck(true) - await evmRevert( - registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()), - 'insufficient funds', - ) - }) - - context('when the registration is funded', () => { - beforeEach(async () => { - await linkToken.connect(keeper1).approve(registry.address, toWei('100')) - await registry.connect(keeper1).addFunds(id, toWei('100')) - }) - - it('reverts if executed', async () => { - await mock.setCanPerform(true) - await mock.setCanCheck(true) - await evmRevert( - registry.checkUpkeep(id, await keeper1.getAddress()), - 'only for simulated backend', - ) - }) - - it('reverts if the specified keeper is not valid', async () => { - await mock.setCanPerform(true) - await mock.setCanCheck(true) - await evmRevert( - registry.checkUpkeep(id, await owner.getAddress()), - 'only for simulated backend', - ) - }) - - context('and upkeep is not needed', () => { - beforeEach(async () => { - await mock.setCanCheck(false) - }) - - it('reverts', async () => { - await evmRevert( - registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()), - 'upkeep not needed', - ) - }) - }) - - context('and the upkeep check fails', () => { - beforeEach(async () => { - const reverter = await upkeepReverterFactory.deploy() - const tx = await registry - .connect(owner) - .registerUpkeep( - reverter.address, - 2500000, - await admin.getAddress(), - emptyBytes, - ) - id = await getUpkeepID(tx) - await linkToken - .connect(keeper1) - .approve(registry.address, toWei('100')) - await registry.connect(keeper1).addFunds(id, toWei('100')) - }) - - it('reverts', async () => { - await evmRevert( - registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()), - 'call to check target failed', - ) - }) - }) - - context('and upkeep check simulations succeeds', () => { - beforeEach(async () => { - await mock.setCanCheck(true) - await mock.setCanPerform(true) - }) - - context('and the registry is paused', () => { - beforeEach(async () => { - await registry.connect(owner).pause() - }) - - it('reverts', async () => { - await evmRevert( - registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()), - 'Pausable: paused', - ) - - await registry.connect(owner).unpause() - - await registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()) - }) - }) - - it('returns true with pricing info if the target can execute', async () => { - const newGasMultiplier = BigNumber.from(10) - await registry - .connect(owner) - .setConfig( - paymentPremiumPPB, - flatFeeMicroLink, - blockCountPerTurn, - maxCheckGas, - stalenessSeconds, - newGasMultiplier, - fallbackGasPrice, - fallbackLinkPrice, - ) - const response = await registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()) - assert.isTrue(response.gasLimit.eq(executeGas)) - assert.isTrue(response.linkEth.eq(linkEth)) - assert.isTrue( - response.adjustedGasWei.eq(gasWei.mul(newGasMultiplier)), - ) - assert.isTrue( - response.maxLinkPayment.eq( - linkForGas(executeGas.toNumber()).mul(newGasMultiplier), - ), - ) - }) - - it('has a large enough gas overhead to cover upkeeps that use all their gas [ @skip-coverage ]', async () => { - await mock.setCheckGasToBurn(maxCheckGas) - await mock.setPerformGasToBurn(executeGas) - const gas = maxCheckGas - .add(executeGas) - .add(PERFORM_GAS_OVERHEAD) - .add(CHECK_GAS_OVERHEAD) - await registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress(), { - gasLimit: gas, - }) - }) - }) - }) - }) - - describe('#performUpkeep', () => { - let _lastKeeper = keeper1 - async function getPerformPaymentAmount() { - _lastKeeper = _lastKeeper === keeper1 ? keeper2 : keeper1 - const before = ( - await registry.getKeeperInfo(await _lastKeeper.getAddress()) - ).balance - await registry.connect(_lastKeeper).performUpkeep(id, '0x') - const after = ( - await registry.getKeeperInfo(await _lastKeeper.getAddress()) - ).balance - const difference = after.sub(before) - return difference - } - - it('reverts if the registration is not funded', async () => { - await evmRevert( - registry.connect(keeper2).performUpkeep(id, '0x'), - 'insufficient funds', - ) - }) - - context('when the registration is funded', () => { - beforeEach(async () => { - await linkToken.connect(owner).approve(registry.address, toWei('100')) - await registry.connect(owner).addFunds(id, toWei('100')) - }) - - it('does not revert if the target cannot execute', async () => { - const mockResponse = await mock - .connect(zeroAddress) - .callStatic.checkUpkeep('0x') - assert.isFalse(mockResponse.callable) - - await registry.connect(keeper3).performUpkeep(id, '0x') - }) - - it('returns false if the target cannot execute', async () => { - const mockResponse = await mock - .connect(zeroAddress) - .callStatic.checkUpkeep('0x') - assert.isFalse(mockResponse.callable) - - assert.isFalse( - await registry.connect(keeper1).callStatic.performUpkeep(id, '0x'), - ) - }) - - it('returns true if called', async () => { - await mock.setCanPerform(true) - - const response = await registry - .connect(keeper1) - .callStatic.performUpkeep(id, '0x') - assert.isTrue(response) - }) - - it('reverts if not enough gas supplied', async () => { - await mock.setCanPerform(true) - - await evmRevert( - registry - .connect(keeper1) - .performUpkeep(id, '0x', { gasLimit: BigNumber.from('120000') }), - ) - }) - - it('executes the data passed to the registry', async () => { - await mock.setCanPerform(true) - - const performData = '0xc0ffeec0ffee' - const tx = await registry - .connect(keeper1) - .performUpkeep(id, performData, { gasLimit: extraGas }) - const receipt = await tx.wait() - const eventLog = receipt?.events - - assert.equal(eventLog?.length, 2) - assert.equal(eventLog?.[1].event, 'UpkeepPerformed') - assert.equal(eventLog?.[1].args?.[0].toNumber(), id.toNumber()) - assert.equal(eventLog?.[1].args?.[1], true) - assert.equal(eventLog?.[1].args?.[2], await keeper1.getAddress()) - assert.isNotEmpty(eventLog?.[1].args?.[3]) - assert.equal(eventLog?.[1].args?.[4], performData) - }) - - it('updates payment balances', async () => { - const keeperBefore = await registry.getKeeperInfo( - await keeper1.getAddress(), - ) - const registrationBefore = await registry.getUpkeep(id) - const keeperLinkBefore = await linkToken.balanceOf( - await keeper1.getAddress(), - ) - const registryLinkBefore = await linkToken.balanceOf(registry.address) - - // Do the thing - await registry.connect(keeper1).performUpkeep(id, '0x') - - const keeperAfter = await registry.getKeeperInfo( - await keeper1.getAddress(), - ) - const registrationAfter = await registry.getUpkeep(id) - const keeperLinkAfter = await linkToken.balanceOf( - await keeper1.getAddress(), - ) - const registryLinkAfter = await linkToken.balanceOf(registry.address) - - assert.isTrue(keeperAfter.balance.gt(keeperBefore.balance)) - assert.isTrue(registrationBefore.balance.gt(registrationAfter.balance)) - assert.isTrue(keeperLinkAfter.eq(keeperLinkBefore)) - assert.isTrue(registryLinkBefore.eq(registryLinkAfter)) - }) - - it('only pays for gas used [ @skip-coverage ]', async () => { - const before = ( - await registry.getKeeperInfo(await keeper1.getAddress()) - ).balance - const tx = await registry.connect(keeper1).performUpkeep(id, '0x') - const receipt = await tx.wait() - const after = (await registry.getKeeperInfo(await keeper1.getAddress())) - .balance - - const max = linkForGas(executeGas.toNumber()) - const totalTx = linkForGas(receipt.gasUsed.toNumber()) - const difference = after.sub(before) - assert.isTrue(max.gt(totalTx)) - assert.isTrue(totalTx.gt(difference)) - assert.isTrue(linkForGas(5700).lt(difference)) // exact number is flaky - assert.isTrue(linkForGas(6000).gt(difference)) // instead test a range - }) - - it('only pays at a rate up to the gas ceiling [ @skip-coverage ]', async () => { - const multiplier = BigNumber.from(10) - const gasPrice = BigNumber.from('1000000000') // 10M x the gas feed's rate - await registry - .connect(owner) - .setConfig( - paymentPremiumPPB, - flatFeeMicroLink, - blockCountPerTurn, - maxCheckGas, - stalenessSeconds, - multiplier, - fallbackGasPrice, - fallbackLinkPrice, - ) - - const before = ( - await registry.getKeeperInfo(await keeper1.getAddress()) - ).balance - const tx = await registry - .connect(keeper1) - .performUpkeep(id, '0x', { gasPrice }) - const receipt = await tx.wait() - const after = (await registry.getKeeperInfo(await keeper1.getAddress())) - .balance - - const max = linkForGas(executeGas).mul(multiplier) - const totalTx = linkForGas(receipt.gasUsed).mul(multiplier) - const difference = after.sub(before) - assert.isTrue(max.gt(totalTx)) - assert.isTrue(totalTx.gt(difference)) - assert.isTrue(linkForGas(5700).mul(multiplier).lt(difference)) - assert.isTrue(linkForGas(6000).mul(multiplier).gt(difference)) - }) - - it('only pays as much as the node spent [ @skip-coverage ]', async () => { - const multiplier = BigNumber.from(10) - const gasPrice = BigNumber.from(200) // 2X the gas feed's rate - const effectiveMultiplier = BigNumber.from(2) - await registry - .connect(owner) - .setConfig( - paymentPremiumPPB, - flatFeeMicroLink, - blockCountPerTurn, - maxCheckGas, - stalenessSeconds, - multiplier, - fallbackGasPrice, - fallbackLinkPrice, - ) - - const before = ( - await registry.getKeeperInfo(await keeper1.getAddress()) - ).balance - const tx = await registry - .connect(keeper1) - .performUpkeep(id, '0x', { gasPrice }) - const receipt = await tx.wait() - const after = (await registry.getKeeperInfo(await keeper1.getAddress())) - .balance - - const max = linkForGas(executeGas.toNumber()).mul(effectiveMultiplier) - const totalTx = linkForGas(receipt.gasUsed).mul(effectiveMultiplier) - const difference = after.sub(before) - assert.isTrue(max.gt(totalTx)) - assert.isTrue(totalTx.gt(difference)) - assert.isTrue(linkForGas(5700).mul(effectiveMultiplier).lt(difference)) - assert.isTrue(linkForGas(6000).mul(effectiveMultiplier).gt(difference)) - }) - - it('pays the caller even if the target function fails', async () => { - const tx = await registry - .connect(owner) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ) - const id = await getUpkeepID(tx) - await linkToken.connect(owner).approve(registry.address, toWei('100')) - await registry.connect(owner).addFunds(id, toWei('100')) - const keeperBalanceBefore = ( - await registry.getKeeperInfo(await keeper1.getAddress()) - ).balance - - // Do the thing - await registry.connect(keeper1).performUpkeep(id, '0x') - - const keeperBalanceAfter = ( - await registry.getKeeperInfo(await keeper1.getAddress()) - ).balance - assert.isTrue(keeperBalanceAfter.gt(keeperBalanceBefore)) - }) - - it('reverts if called by a non-keeper', async () => { - await evmRevert( - registry.connect(nonkeeper).performUpkeep(id, '0x'), - 'only active keepers', - ) - }) - - it('reverts if the upkeep has been canceled', async () => { - await mock.setCanPerform(true) - - await registry.connect(owner).cancelUpkeep(id) - - await evmRevert( - registry.connect(keeper1).performUpkeep(id, '0x'), - 'invalid upkeep id', - ) - }) - - it('uses the fallback gas price if the feed price is stale [ @skip-coverage ]', async () => { - const normalAmount = await getPerformPaymentAmount() - const roundId = 99 - const answer = 100 - const updatedAt = 946684800 // New Years 2000 🥳 - const startedAt = 946684799 - await gasPriceFeed - .connect(owner) - .updateRoundData(roundId, answer, updatedAt, startedAt) - const amountWithStaleFeed = await getPerformPaymentAmount() - assert.isTrue(normalAmount.lt(amountWithStaleFeed)) - }) - - it('uses the fallback gas price if the feed price is non-sensical [ @skip-coverage ]', async () => { - const normalAmount = await getPerformPaymentAmount() - const roundId = 99 - const updatedAt = Math.floor(Date.now() / 1000) - const startedAt = 946684799 - await gasPriceFeed - .connect(owner) - .updateRoundData(roundId, -100, updatedAt, startedAt) - const amountWithNegativeFeed = await getPerformPaymentAmount() - await gasPriceFeed - .connect(owner) - .updateRoundData(roundId, 0, updatedAt, startedAt) - const amountWithZeroFeed = await getPerformPaymentAmount() - assert.isTrue(normalAmount.lt(amountWithNegativeFeed)) - assert.isTrue(normalAmount.lt(amountWithZeroFeed)) - }) - - it('uses the fallback if the link price feed is stale', async () => { - const normalAmount = await getPerformPaymentAmount() - const roundId = 99 - const answer = 100 - const updatedAt = 946684800 // New Years 2000 🥳 - const startedAt = 946684799 - await linkEthFeed - .connect(owner) - .updateRoundData(roundId, answer, updatedAt, startedAt) - const amountWithStaleFeed = await getPerformPaymentAmount() - assert.isTrue(normalAmount.lt(amountWithStaleFeed)) - }) - - it('uses the fallback link price if the feed price is non-sensical', async () => { - const normalAmount = await getPerformPaymentAmount() - const roundId = 99 - const updatedAt = Math.floor(Date.now() / 1000) - const startedAt = 946684799 - await linkEthFeed - .connect(owner) - .updateRoundData(roundId, -100, updatedAt, startedAt) - const amountWithNegativeFeed = await getPerformPaymentAmount() - await linkEthFeed - .connect(owner) - .updateRoundData(roundId, 0, updatedAt, startedAt) - const amountWithZeroFeed = await getPerformPaymentAmount() - assert.isTrue(normalAmount.lt(amountWithNegativeFeed)) - assert.isTrue(normalAmount.lt(amountWithZeroFeed)) - }) - - it('reverts if the same caller calls twice in a row', async () => { - await registry.connect(keeper1).performUpkeep(id, '0x') - await evmRevert( - registry.connect(keeper1).performUpkeep(id, '0x'), - 'keepers must take turns', - ) - await registry.connect(keeper2).performUpkeep(id, '0x') - await evmRevert( - registry.connect(keeper2).performUpkeep(id, '0x'), - 'keepers must take turns', - ) - await registry.connect(keeper1).performUpkeep(id, '0x') - }) - - it('has a large enough gas overhead to cover upkeeps that use all their gas [ @skip-coverage ]', async () => { - await mock.setPerformGasToBurn(executeGas) - await mock.setCanPerform(true) - const gas = executeGas.add(PERFORM_GAS_OVERHEAD) - const performData = '0xc0ffeec0ffee' - const tx = await registry - .connect(keeper1) - .performUpkeep(id, performData, { gasLimit: gas }) - const receipt = await tx.wait() - const eventLog = receipt?.events - - assert.equal(eventLog?.length, 2) - assert.equal(eventLog?.[1].event, 'UpkeepPerformed') - assert.equal(eventLog?.[1].args?.[0].toNumber(), id.toNumber()) - assert.equal(eventLog?.[1].args?.[1], true) - assert.equal(eventLog?.[1].args?.[2], await keeper1.getAddress()) - assert.isNotEmpty(eventLog?.[1].args?.[3]) - assert.equal(eventLog?.[1].args?.[4], performData) - }) - }) - }) - - describe('#withdrawFunds', () => { - beforeEach(async () => { - await linkToken.connect(keeper1).approve(registry.address, toWei('100')) - await registry.connect(keeper1).addFunds(id, toWei('1')) - }) - - it('reverts if called by anyone but the admin', async () => { - await evmRevert( - registry - .connect(owner) - .withdrawFunds(id.add(1).toNumber(), await payee1.getAddress()), - 'only callable by admin', - ) - }) - - it('reverts if called on an uncanceled upkeep', async () => { - await evmRevert( - registry.connect(admin).withdrawFunds(id, await payee1.getAddress()), - 'upkeep must be canceled', - ) - }) - - it('reverts if called with the 0 address', async () => { - await evmRevert( - registry.connect(admin).withdrawFunds(id, zeroAddress), - 'cannot send to zero address', - ) - }) - - describe('after the registration is cancelled', () => { - beforeEach(async () => { - await registry.connect(owner).cancelUpkeep(id) - }) - - it('moves the funds out and updates the balance', async () => { - const payee1Before = await linkToken.balanceOf( - await payee1.getAddress(), - ) - const registryBefore = await linkToken.balanceOf(registry.address) - - let registration = await registry.getUpkeep(id) - assert.isTrue(toWei('1').eq(registration.balance)) - - await registry - .connect(admin) - .withdrawFunds(id, await payee1.getAddress()) - - const payee1After = await linkToken.balanceOf(await payee1.getAddress()) - const registryAfter = await linkToken.balanceOf(registry.address) - - assert.isTrue(payee1Before.add(toWei('1')).eq(payee1After)) - assert.isTrue(registryBefore.sub(toWei('1')).eq(registryAfter)) - - registration = await registry.getUpkeep(id) - assert.equal(0, registration.balance.toNumber()) - }) - }) - }) - - describe('#cancelUpkeep', () => { - it('reverts if the ID is not valid', async () => { - await evmRevert( - registry.connect(owner).cancelUpkeep(id.add(1).toNumber()), - 'too late to cancel upkeep', - ) - }) - - it('reverts if called by a non-owner/non-admin', async () => { - await evmRevert( - registry.connect(keeper1).cancelUpkeep(id), - 'only owner or admin', - ) - }) - - describe('when called by the owner', async () => { - it('sets the registration to invalid immediately', async () => { - const tx = await registry.connect(owner).cancelUpkeep(id) - const receipt = await tx.wait() - const registration = await registry.getUpkeep(id) - assert.equal( - registration.maxValidBlocknumber.toNumber(), - receipt.blockNumber, - ) - }) - - it('emits an event', async () => { - const tx = await registry.connect(owner).cancelUpkeep(id) - const receipt = await tx.wait() - await expect(tx) - .to.emit(registry, 'UpkeepCanceled') - .withArgs(id, BigNumber.from(receipt.blockNumber)) - }) - - it('updates the canceled registrations list', async () => { - let canceled = await registry.callStatic.getCanceledUpkeepList() - assert.deepEqual([], canceled) - - await registry.connect(owner).cancelUpkeep(id) - - canceled = await registry.callStatic.getCanceledUpkeepList() - assert.deepEqual([id], canceled) - }) - - it('immediately prevents upkeep', async () => { - await registry.connect(owner).cancelUpkeep(id) - - await evmRevert( - registry.connect(keeper2).performUpkeep(id, '0x'), - 'invalid upkeep id', - ) - }) - - it('does not revert if reverts if called multiple times', async () => { - await registry.connect(owner).cancelUpkeep(id) - await evmRevert( - registry.connect(owner).cancelUpkeep(id), - 'too late to cancel upkeep', - ) - }) - - describe('when called by the owner when the admin has just canceled', () => { - let oldExpiration: BigNumber - - beforeEach(async () => { - await registry.connect(admin).cancelUpkeep(id) - const registration = await registry.getUpkeep(id) - oldExpiration = registration.maxValidBlocknumber - }) - - it('allows the owner to cancel it more quickly', async () => { - await registry.connect(owner).cancelUpkeep(id) - - const registration = await registry.getUpkeep(id) - const newExpiration = registration.maxValidBlocknumber - assert.isTrue(newExpiration.lt(oldExpiration)) - }) - }) - }) - - describe('when called by the admin', async () => { - const delay = 50 - - it('sets the registration to invalid in 50 blocks', async () => { - const tx = await registry.connect(admin).cancelUpkeep(id) - const receipt = await tx.wait() - const registration = await registry.getUpkeep(id) - assert.equal( - registration.maxValidBlocknumber.toNumber(), - receipt.blockNumber + 50, - ) - }) - - it('emits an event', async () => { - const tx = await registry.connect(admin).cancelUpkeep(id) - const receipt = await tx.wait() - await expect(tx) - .to.emit(registry, 'UpkeepCanceled') - .withArgs(id, BigNumber.from(receipt.blockNumber + delay)) - }) - - it('updates the canceled registrations list', async () => { - let canceled = await registry.callStatic.getCanceledUpkeepList() - assert.deepEqual([], canceled) - - await registry.connect(admin).cancelUpkeep(id) - - canceled = await registry.callStatic.getCanceledUpkeepList() - assert.deepEqual([id], canceled) - }) - - it('immediately prevents upkeep', async () => { - await linkToken.connect(owner).approve(registry.address, toWei('100')) - await registry.connect(owner).addFunds(id, toWei('100')) - await registry.connect(admin).cancelUpkeep(id) - await registry.connect(keeper2).performUpkeep(id, '0x') // still works - - for (let i = 0; i < delay; i++) { - await ethers.provider.send('evm_mine', []) - } - - await evmRevert( - registry.connect(keeper2).performUpkeep(id, '0x'), - 'invalid upkeep id', - ) - }) - - it('reverts if called again by the admin', async () => { - await registry.connect(admin).cancelUpkeep(id) - - await evmRevert( - registry.connect(admin).cancelUpkeep(id), - 'too late to cancel upkeep', - ) - }) - - it('does not revert or double add the cancellation record if called by the owner immediately after', async () => { - await registry.connect(admin).cancelUpkeep(id) - - await registry.connect(owner).cancelUpkeep(id) - - const canceled = await registry.callStatic.getCanceledUpkeepList() - assert.deepEqual([id], canceled) - }) - - it('reverts if called by the owner after the timeout', async () => { - await registry.connect(admin).cancelUpkeep(id) - - for (let i = 0; i < delay; i++) { - await ethers.provider.send('evm_mine', []) - } - - await evmRevert( - registry.connect(owner).cancelUpkeep(id), - 'too late to cancel upkeep', - ) - }) - }) - }) - - describe('#withdrawPayment', () => { - beforeEach(async () => { - await linkToken.connect(owner).approve(registry.address, toWei('100')) - await registry.connect(owner).addFunds(id, toWei('100')) - await registry.connect(keeper1).performUpkeep(id, '0x') - }) - - it('reverts if called by anyone but the payee', async () => { - await evmRevert( - registry - .connect(payee2) - .withdrawPayment( - await keeper1.getAddress(), - await nonkeeper.getAddress(), - ), - 'only callable by payee', - ) - }) - - it('reverts if called with the 0 address', async () => { - await evmRevert( - registry - .connect(payee2) - .withdrawPayment(await keeper1.getAddress(), zeroAddress), - 'cannot send to zero address', - ) - }) - - it('updates the balances', async () => { - const to = await nonkeeper.getAddress() - const keeperBefore = ( - await registry.getKeeperInfo(await keeper1.getAddress()) - ).balance - const registrationBefore = (await registry.getUpkeep(id)).balance - const toLinkBefore = await linkToken.balanceOf(to) - const registryLinkBefore = await linkToken.balanceOf(registry.address) - - //// Do the thing - await registry - .connect(payee1) - .withdrawPayment(await keeper1.getAddress(), to) - - const keeperAfter = ( - await registry.getKeeperInfo(await keeper1.getAddress()) - ).balance - const registrationAfter = (await registry.getUpkeep(id)).balance - const toLinkAfter = await linkToken.balanceOf(to) - const registryLinkAfter = await linkToken.balanceOf(registry.address) - - assert.isTrue(keeperAfter.eq(BigNumber.from(0))) - assert.isTrue(registrationBefore.eq(registrationAfter)) - assert.isTrue(toLinkBefore.add(keeperBefore).eq(toLinkAfter)) - assert.isTrue(registryLinkBefore.sub(keeperBefore).eq(registryLinkAfter)) - }) - - it('emits a log announcing the withdrawal', async () => { - const balance = (await registry.getKeeperInfo(await keeper1.getAddress())) - .balance - const tx = await registry - .connect(payee1) - .withdrawPayment( - await keeper1.getAddress(), - await nonkeeper.getAddress(), - ) - await expect(tx) - .to.emit(registry, 'PaymentWithdrawn') - .withArgs( - await keeper1.getAddress(), - balance, - await nonkeeper.getAddress(), - await payee1.getAddress(), - ) - }) - }) - - describe('#transferPayeeship', () => { - it('reverts when called by anyone but the current payee', async () => { - await evmRevert( - registry - .connect(payee2) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ), - 'only callable by payee', - ) - }) - - it('reverts when transferring to self', async () => { - await evmRevert( - registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee1.getAddress(), - ), - 'cannot transfer to self', - ) - }) - - it('does not change the payee', async () => { - await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - - const info = await registry.getKeeperInfo(await keeper1.getAddress()) - assert.equal(await payee1.getAddress(), info.payee) - }) - - it('emits an event announcing the new payee', async () => { - const tx = await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - await expect(tx) - .to.emit(registry, 'PayeeshipTransferRequested') - .withArgs( - await keeper1.getAddress(), - await payee1.getAddress(), - await payee2.getAddress(), - ) - }) - - it('does not emit an event when called with the same proposal', async () => { - await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - - const tx = await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - const receipt = await tx.wait() - assert.equal(0, receipt.logs.length) - }) - }) - - describe('#acceptPayeeship', () => { - beforeEach(async () => { - await registry - .connect(payee1) - .transferPayeeship( - await keeper1.getAddress(), - await payee2.getAddress(), - ) - }) - - it('reverts when called by anyone but the proposed payee', async () => { - await evmRevert( - registry.connect(payee1).acceptPayeeship(await keeper1.getAddress()), - 'only callable by proposed payee', - ) - }) - - it('emits an event announcing the new payee', async () => { - const tx = await registry - .connect(payee2) - .acceptPayeeship(await keeper1.getAddress()) - await expect(tx) - .to.emit(registry, 'PayeeshipTransferred') - .withArgs( - await keeper1.getAddress(), - await payee1.getAddress(), - await payee2.getAddress(), - ) - }) - - it('does change the payee', async () => { - await registry.connect(payee2).acceptPayeeship(await keeper1.getAddress()) - - const info = await registry.getKeeperInfo(await keeper1.getAddress()) - assert.equal(await payee2.getAddress(), info.payee) - }) - }) - - describe('#setConfig', () => { - const payment = BigNumber.from(1) - const flatFee = BigNumber.from(2) - const checks = BigNumber.from(3) - const staleness = BigNumber.from(4) - const ceiling = BigNumber.from(5) - const maxGas = BigNumber.from(6) - const fbGasEth = BigNumber.from(7) - const fbLinkEth = BigNumber.from(8) - - it('reverts when called by anyone but the proposed owner', async () => { - await evmRevert( - registry - .connect(payee1) - .setConfig( - payment, - flatFee, - checks, - maxGas, - staleness, - gasCeilingMultiplier, - fbGasEth, - fbLinkEth, - ), - 'Only callable by owner', - ) - }) - - it('updates the config', async () => { - const old = await registry.getConfig() - const oldFlatFee = await registry.getFlatFee() - assert.isTrue(paymentPremiumPPB.eq(old.paymentPremiumPPB)) - assert.isTrue(flatFeeMicroLink.eq(oldFlatFee)) - assert.isTrue(blockCountPerTurn.eq(old.blockCountPerTurn)) - assert.isTrue(stalenessSeconds.eq(old.stalenessSeconds)) - assert.isTrue(gasCeilingMultiplier.eq(old.gasCeilingMultiplier)) - - await registry - .connect(owner) - .setConfig( - payment, - flatFee, - checks, - maxGas, - staleness, - ceiling, - fbGasEth, - fbLinkEth, - ) - - const updated = await registry.getConfig() - const newFlatFee = await registry.getFlatFee() - assert.equal(updated.paymentPremiumPPB, payment.toNumber()) - assert.equal(newFlatFee, flatFee.toNumber()) - assert.equal(updated.blockCountPerTurn, checks.toNumber()) - assert.equal(updated.stalenessSeconds, staleness.toNumber()) - assert.equal(updated.gasCeilingMultiplier, ceiling.toNumber()) - assert.equal(updated.checkGasLimit, maxGas.toNumber()) - assert.equal(updated.fallbackGasPrice.toNumber(), fbGasEth.toNumber()) - assert.equal(updated.fallbackLinkPrice.toNumber(), fbLinkEth.toNumber()) - }) - - it('emits an event', async () => { - const tx = await registry - .connect(owner) - .setConfig( - payment, - flatFee, - checks, - maxGas, - staleness, - ceiling, - fbGasEth, - fbLinkEth, - ) - await expect(tx) - .to.emit(registry, 'ConfigSet') - .withArgs( - payment, - checks, - maxGas, - staleness, - ceiling, - fbGasEth, - fbLinkEth, - ) - }) - }) - - describe('#onTokenTransfer', () => { - const amount = toWei('1') - - it('reverts if not called by the LINK token', async () => { - const data = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [id.toNumber().toString()], - ) - - await evmRevert( - registry - .connect(keeper1) - .onTokenTransfer(await keeper1.getAddress(), amount, data), - 'only callable through LINK', - ) - }) - - it('reverts if not called with more or less than 32 bytes', async () => { - const longData = ethers.utils.defaultAbiCoder.encode( - ['uint256', 'uint256'], - ['33', '34'], - ) - const shortData = '0x12345678' - - await evmRevert( - linkToken - .connect(owner) - .transferAndCall(registry.address, amount, longData), - ) - await evmRevert( - linkToken - .connect(owner) - .transferAndCall(registry.address, amount, shortData), - ) - }) - - it('reverts if the upkeep is canceled', async () => { - await registry.connect(admin).cancelUpkeep(id) - await evmRevert( - registry.connect(keeper1).addFunds(id, amount), - 'upkeep must be active', - ) - }) - - it('updates the funds of the job id passed', async () => { - const data = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [id.toNumber().toString()], - ) - - const before = (await registry.getUpkeep(id)).balance - await linkToken - .connect(owner) - .transferAndCall(registry.address, amount, data) - const after = (await registry.getUpkeep(id)).balance - - assert.isTrue(before.add(amount).eq(after)) - }) - }) - - describe('#recoverFunds', () => { - const sent = toWei('7') - - beforeEach(async () => { - await linkToken.connect(keeper1).approve(registry.address, toWei('100')) - - // add funds to upkeep 1 and perform and withdraw some payment - const tx = await registry - .connect(owner) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ) - const id1 = await getUpkeepID(tx) - await registry.connect(keeper1).addFunds(id1, toWei('5')) - await registry.connect(keeper1).performUpkeep(id1, '0x') - await registry.connect(keeper2).performUpkeep(id1, '0x') - await registry.connect(keeper3).performUpkeep(id1, '0x') - await registry - .connect(payee1) - .withdrawPayment( - await keeper1.getAddress(), - await nonkeeper.getAddress(), - ) - - // transfer funds directly to the registry - await linkToken.connect(keeper1).transfer(registry.address, sent) - - // add funds to upkeep 2 and perform and withdraw some payment - const tx2 = await registry - .connect(owner) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ) - const id2 = await getUpkeepID(tx2) - await registry.connect(keeper1).addFunds(id2, toWei('5')) - await registry.connect(keeper1).performUpkeep(id2, '0x') - await registry.connect(keeper2).performUpkeep(id2, '0x') - await registry.connect(keeper3).performUpkeep(id2, '0x') - await registry - .connect(payee2) - .withdrawPayment( - await keeper2.getAddress(), - await nonkeeper.getAddress(), - ) - - // transfer funds using onTokenTransfer - const data = ethers.utils.defaultAbiCoder.encode( - ['uint256'], - [id2.toNumber().toString()], - ) - await linkToken - .connect(owner) - .transferAndCall(registry.address, toWei('1'), data) - - // remove a keeper - await registry - .connect(owner) - .setKeepers( - [await keeper1.getAddress(), await keeper2.getAddress()], - [await payee1.getAddress(), await payee2.getAddress()], - ) - - // withdraw some funds - await registry.connect(owner).cancelUpkeep(id1) - await registry.connect(admin).withdrawFunds(id1, await admin.getAddress()) - }) - - it('reverts if not called by owner', async () => { - await evmRevert( - registry.connect(keeper1).recoverFunds(), - 'Only callable by owner', - ) - }) - - it('allows any funds that have been accidentally transfered to be moved', async () => { - const balanceBefore = await linkToken.balanceOf(registry.address) - - await linkToken.balanceOf(registry.address) - - await registry.connect(owner).recoverFunds() - const balanceAfter = await linkToken.balanceOf(registry.address) - assert.isTrue(balanceBefore.eq(balanceAfter.add(sent))) - }) - }) - - describe('#pause', () => { - it('reverts if called by a non-owner', async () => { - await evmRevert( - registry.connect(keeper1).pause(), - 'Only callable by owner', - ) - }) - - it('marks the contract as paused', async () => { - assert.isFalse(await registry.paused()) - - await registry.connect(owner).pause() - - assert.isTrue(await registry.paused()) - }) - }) - - describe('#unpause', () => { - beforeEach(async () => { - await registry.connect(owner).pause() - }) - - it('reverts if called by a non-owner', async () => { - await evmRevert( - registry.connect(keeper1).unpause(), - 'Only callable by owner', - ) - }) - - it('marks the contract as not paused', async () => { - assert.isTrue(await registry.paused()) - - await registry.connect(owner).unpause() - - assert.isFalse(await registry.paused()) - }) - }) - - describe('#getMaxPaymentForGas', () => { - const gasAmounts = [100000, 10000000] - const premiums = [0, 250000000] - const flatFees = [0, 1000000] - it('calculates the max fee approptiately', async () => { - for (let idx = 0; idx < gasAmounts.length; idx++) { - const gas = gasAmounts[idx] - for (let jdx = 0; jdx < premiums.length; jdx++) { - const premium = premiums[jdx] - for (let kdx = 0; kdx < flatFees.length; kdx++) { - const flatFee = flatFees[kdx] - await registry - .connect(owner) - .setConfig( - premium, - flatFee, - blockCountPerTurn, - maxCheckGas, - stalenessSeconds, - gasCeilingMultiplier, - fallbackGasPrice, - fallbackLinkPrice, - ) - const price = await registry.getMaxPaymentForGas(gas) - expect(price).to.equal(linkForGas(gas, premium, flatFee)) - } - } - } - }) - }) - - describe('#checkUpkeep / #performUpkeep', () => { - const performData = '0xc0ffeec0ffee' - const multiplier = BigNumber.from(10) - const flatFee = BigNumber.from('100000') //0.1 LINK - const callGasPrice = 1 - - it('uses the same minimum balance calculation [ @skip-coverage ]', async () => { - await registry - .connect(owner) - .setConfig( - paymentPremiumPPB, - flatFee, - blockCountPerTurn, - maxCheckGas, - stalenessSeconds, - multiplier, - fallbackGasPrice, - fallbackLinkPrice, - ) - await linkToken.connect(owner).approve(registry.address, toWei('100')) - - const tx1 = await registry - .connect(owner) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ) - const upkeepID1 = await getUpkeepID(tx1) - const tx2 = await registry - .connect(owner) - .registerUpkeep( - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - ) - const upkeepID2 = await getUpkeepID(tx2) - await mock.setCanCheck(true) - await mock.setCanPerform(true) - // upkeep 1 is underfunded, 2 is funded - const minBalance1 = (await registry.getMaxPaymentForGas(executeGas)).sub( - 1, - ) - const minBalance2 = await registry.getMaxPaymentForGas(executeGas) - await registry.connect(owner).addFunds(upkeepID1, minBalance1) - await registry.connect(owner).addFunds(upkeepID2, minBalance2) - // upkeep 1 check should revert, 2 should succeed - await evmRevert( - registry - .connect(zeroAddress) - .callStatic.checkUpkeep(upkeepID1, await keeper1.getAddress(), { - gasPrice: callGasPrice, - }), - ) - await registry - .connect(zeroAddress) - .callStatic.checkUpkeep(upkeepID2, await keeper1.getAddress(), { - gasPrice: callGasPrice, - }) - // upkeep 1 perform should revert, 2 should succeed - await evmRevert( - registry - .connect(keeper1) - .performUpkeep(upkeepID1, performData, { gasLimit: extraGas }), - 'insufficient funds', - ) - await registry - .connect(keeper1) - .performUpkeep(upkeepID2, performData, { gasLimit: extraGas }) - }) - }) - - describe('#getMinBalanceForUpkeep / #checkUpkeep', () => { - it('calculates the minimum balance appropriately', async () => { - const oneWei = BigNumber.from('1') - await linkToken.connect(keeper1).approve(registry.address, toWei('100')) - await mock.setCanCheck(true) - await mock.setCanPerform(true) - const minBalance = await registry.getMinBalanceForUpkeep(id) - const tooLow = minBalance.sub(oneWei) - await registry.connect(keeper1).addFunds(id, tooLow) - await evmRevert( - registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()), - 'insufficient funds', - ) - await registry.connect(keeper1).addFunds(id, oneWei) - await registry - .connect(zeroAddress) - .callStatic.checkUpkeep(id, await keeper1.getAddress()) - }) - }) -}) diff --git a/contracts/test/v0.7/Operator.test.ts b/contracts/test/v0.7/Operator.test.ts deleted file mode 100644 index 4af846576b3..00000000000 --- a/contracts/test/v0.7/Operator.test.ts +++ /dev/null @@ -1,3819 +0,0 @@ -import { ethers } from 'hardhat' -import { - publicAbi, - toBytes32String, - toWei, - stringToBytes, - increaseTime5Minutes, - getLog, -} from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { - BigNumber, - constants, - Contract, - ContractFactory, - ContractReceipt, - ContractTransaction, - Signer, -} from 'ethers' -import { getUsers, Roles } from '../test-helpers/setup' -import { bigNumEquals, evmRevert } from '../test-helpers/matchers' -import type { providers } from 'ethers' -import { - convertCancelParams, - convertCancelByRequesterParams, - convertFufillParams, - convertFulfill2Params, - decodeRunRequest, - encodeOracleRequest, - encodeRequestOracleData, - RunRequest, -} from '../test-helpers/oracle' - -let v7ConsumerFactory: ContractFactory -let basicConsumerFactory: ContractFactory -let multiWordConsumerFactory: ContractFactory -let gasGuzzlingConsumerFactory: ContractFactory -let getterSetterFactory: ContractFactory -let maliciousRequesterFactory: ContractFactory -let maliciousConsumerFactory: ContractFactory -let maliciousMultiWordConsumerFactory: ContractFactory -let operatorFactory: ContractFactory -let forwarderFactory: ContractFactory -let linkTokenFactory: ContractFactory -const zeroAddress = ethers.constants.AddressZero - -let roles: Roles - -before(async () => { - const users = await getUsers() - - roles = users.roles - v7ConsumerFactory = await ethers.getContractFactory( - 'src/v0.7/tests/Consumer.sol:Consumer', - ) - basicConsumerFactory = await ethers.getContractFactory( - 'src/v0.6/tests/BasicConsumer.sol:BasicConsumer', - ) - multiWordConsumerFactory = await ethers.getContractFactory( - 'src/v0.7/tests/MultiWordConsumer.sol:MultiWordConsumer', - ) - gasGuzzlingConsumerFactory = await ethers.getContractFactory( - 'src/v0.6/tests/GasGuzzlingConsumer.sol:GasGuzzlingConsumer', - ) - getterSetterFactory = await ethers.getContractFactory( - 'src/v0.4/tests/GetterSetter.sol:GetterSetter', - ) - maliciousRequesterFactory = await ethers.getContractFactory( - 'src/v0.4/tests/MaliciousRequester.sol:MaliciousRequester', - ) - maliciousConsumerFactory = await ethers.getContractFactory( - 'src/v0.4/tests/MaliciousConsumer.sol:MaliciousConsumer', - ) - maliciousMultiWordConsumerFactory = await ethers.getContractFactory( - 'src/v0.6/tests/MaliciousMultiWordConsumer.sol:MaliciousMultiWordConsumer', - ) - operatorFactory = await ethers.getContractFactory( - 'src/v0.7/Operator.sol:Operator', - ) - forwarderFactory = await ethers.getContractFactory( - 'src/v0.7/AuthorizedForwarder.sol:AuthorizedForwarder', - ) - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - ) -}) - -describe('Operator', () => { - let fHash: string - let specId: string - let to: string - let link: Contract - let operator: Contract - let forwarder1: Contract - let forwarder2: Contract - let owner: Signer - - beforeEach(async () => { - fHash = getterSetterFactory.interface.getSighash('requestedBytes32') - specId = - '0x4c7b7ffb66b344fbaa64995af81e355a00000000000000000000000000000000' - to = '0x80e29acb842498fe6591f020bd82766dce619d43' - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - owner = roles.defaultAccount - operator = await operatorFactory - .connect(owner) - .deploy(link.address, await owner.getAddress()) - await operator - .connect(roles.defaultAccount) - .setAuthorizedSenders([await roles.oracleNode.getAddress()]) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(operator, [ - 'acceptAuthorizedReceivers', - 'acceptOwnableContracts', - 'cancelOracleRequest', - 'cancelOracleRequestByRequester', - 'distributeFunds', - 'fulfillOracleRequest', - 'fulfillOracleRequest2', - 'getAuthorizedSenders', - 'getChainlinkToken', - 'getExpiryTime', - 'isAuthorizedSender', - 'onTokenTransfer', - 'operatorRequest', - 'oracleRequest', - 'ownerForward', - 'ownerTransferAndCall', - 'setAuthorizedSenders', - 'setAuthorizedSendersOn', - 'transferOwnableContracts', - 'typeAndVersion', - 'withdraw', - 'withdrawable', - // Ownable methods: - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('#typeAndVersion', () => { - it('describes the operator', async () => { - assert.equal(await operator.typeAndVersion(), 'Operator 1.0.0') - }) - }) - - describe('#transferOwnableContracts', () => { - beforeEach(async () => { - forwarder1 = await forwarderFactory - .connect(owner) - .deploy(link.address, operator.address, zeroAddress, '0x') - forwarder2 = await forwarderFactory - .connect(owner) - .deploy(link.address, operator.address, zeroAddress, '0x') - }) - - describe('being called by the owner', () => { - it('cannot transfer to self', async () => { - await evmRevert( - operator - .connect(owner) - .transferOwnableContracts([forwarder1.address], operator.address), - 'Cannot transfer to self', - ) - }) - - it('emits an ownership transfer request event', async () => { - const tx = await operator - .connect(owner) - .transferOwnableContracts( - [forwarder1.address, forwarder2.address], - await roles.oracleNode1.getAddress(), - ) - const receipt = await tx.wait() - assert.equal(receipt?.events?.length, 2) - const log1 = receipt?.events?.[0] - assert.equal(log1?.event, 'OwnershipTransferRequested') - assert.equal(log1?.address, forwarder1.address) - assert.equal(log1?.args?.[0], operator.address) - assert.equal(log1?.args?.[1], await roles.oracleNode1.getAddress()) - const log2 = receipt?.events?.[1] - assert.equal(log2?.event, 'OwnershipTransferRequested') - assert.equal(log2?.address, forwarder2.address) - assert.equal(log2?.args?.[0], operator.address) - assert.equal(log2?.args?.[1], await roles.oracleNode1.getAddress()) - }) - }) - - describe('being called by a non-owner', () => { - it('reverts with message', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .transferOwnableContracts( - [forwarder1.address], - await roles.oracleNode2.getAddress(), - ), - 'Only callable by owner', - ) - }) - }) - }) - - describe('#acceptOwnableContracts', () => { - describe('being called by the owner', () => { - let operator2: Contract - let receipt: ContractReceipt - - beforeEach(async () => { - operator2 = await operatorFactory - .connect(roles.defaultAccount) - .deploy(link.address, await roles.defaultAccount.getAddress()) - forwarder1 = await forwarderFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, zeroAddress, '0x') - forwarder2 = await forwarderFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, zeroAddress, '0x') - await operator - .connect(roles.defaultAccount) - .transferOwnableContracts( - [forwarder1.address, forwarder2.address], - operator2.address, - ) - const tx = await operator2 - .connect(roles.defaultAccount) - .acceptOwnableContracts([forwarder1.address, forwarder2.address]) - receipt = await tx.wait() - }) - - it('sets the new owner on the forwarder', async () => { - assert.equal(await forwarder1.owner(), operator2.address) - }) - - it('emits ownership transferred events', async () => { - assert.equal(receipt?.events?.[0]?.event, 'OwnableContractAccepted') - assert.equal(receipt?.events?.[0]?.args?.[0], forwarder1.address) - - assert.equal(receipt?.events?.[1]?.event, 'OwnershipTransferred') - assert.equal(receipt?.events?.[1]?.address, forwarder1.address) - assert.equal(receipt?.events?.[1]?.args?.[0], operator.address) - assert.equal(receipt?.events?.[1]?.args?.[1], operator2.address) - - assert.equal(receipt?.events?.[2]?.event, 'OwnableContractAccepted') - assert.equal(receipt?.events?.[2]?.args?.[0], forwarder2.address) - - assert.equal(receipt?.events?.[3]?.event, 'OwnershipTransferred') - assert.equal(receipt?.events?.[3]?.address, forwarder2.address) - assert.equal(receipt?.events?.[3]?.args?.[0], operator.address) - assert.equal(receipt?.events?.[3]?.args?.[1], operator2.address) - }) - }) - - describe('being called by a non-owner authorized sender', () => { - it('does not revert', async () => { - await operator - .connect(roles.defaultAccount) - .setAuthorizedSenders([await roles.oracleNode1.getAddress()]) - - await operator.connect(roles.oracleNode1).acceptOwnableContracts([]) - }) - }) - - describe('being called by a non owner', () => { - it('reverts with message', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .acceptOwnableContracts([await roles.oracleNode2.getAddress()]), - 'Cannot set authorized senders', - ) - }) - }) - }) - - describe('#distributeFunds', () => { - describe('when called with empty arrays', () => { - it('reverts with invalid array message', async () => { - await evmRevert( - operator.connect(roles.defaultAccount).distributeFunds([], []), - 'Invalid array length(s)', - ) - }) - }) - - describe('when called with unequal array lengths', () => { - it('reverts with invalid array message', async () => { - const receivers = [ - await roles.oracleNode2.getAddress(), - await roles.oracleNode3.getAddress(), - ] - const amounts = [1, 2, 3] - await evmRevert( - operator - .connect(roles.defaultAccount) - .distributeFunds(receivers, amounts), - 'Invalid array length(s)', - ) - }) - }) - - describe('when called with not enough ETH', () => { - it('reverts with subtraction overflow message', async () => { - const amountToSend = toWei('2') - const ethSent = toWei('1') - await evmRevert( - operator - .connect(roles.defaultAccount) - .distributeFunds( - [await roles.oracleNode2.getAddress()], - [amountToSend], - { - value: ethSent, - }, - ), - 'SafeMath: subtraction overflow', - ) - }) - }) - - describe('when called with too much ETH', () => { - it('reverts with too much ETH message', async () => { - const amountToSend = toWei('2') - const ethSent = toWei('3') - await evmRevert( - operator - .connect(roles.defaultAccount) - .distributeFunds( - [await roles.oracleNode2.getAddress()], - [amountToSend], - { - value: ethSent, - }, - ), - 'Too much ETH sent', - ) - }) - }) - - describe('when called with correct values', () => { - it('updates the balances', async () => { - const node2BalanceBefore = await roles.oracleNode2.getBalance() - const node3BalanceBefore = await roles.oracleNode3.getBalance() - const receivers = [ - await roles.oracleNode2.getAddress(), - await roles.oracleNode3.getAddress(), - ] - const sendNode2 = toWei('2') - const sendNode3 = toWei('3') - const totalAmount = toWei('5') - const amounts = [sendNode2, sendNode3] - - await operator - .connect(roles.defaultAccount) - .distributeFunds(receivers, amounts, { value: totalAmount }) - - const node2BalanceAfter = await roles.oracleNode2.getBalance() - const node3BalanceAfter = await roles.oracleNode3.getBalance() - - assert.equal( - node2BalanceAfter.sub(node2BalanceBefore).toString(), - sendNode2.toString(), - ) - - assert.equal( - node3BalanceAfter.sub(node3BalanceBefore).toString(), - sendNode3.toString(), - ) - }) - }) - }) - - describe('#setAuthorizedSenders', () => { - let newSenders: string[] - let receipt: ContractReceipt - describe('when called by the owner', () => { - describe('setting 3 authorized senders', () => { - beforeEach(async () => { - newSenders = [ - await roles.oracleNode1.getAddress(), - await roles.oracleNode2.getAddress(), - await roles.oracleNode3.getAddress(), - ] - const tx = await operator - .connect(roles.defaultAccount) - .setAuthorizedSenders(newSenders) - receipt = await tx.wait() - }) - - it('adds the authorized nodes', async () => { - const authorizedSenders = await operator.getAuthorizedSenders() - assert.equal(newSenders.length, authorizedSenders.length) - for (let i = 0; i < authorizedSenders.length; i++) { - assert.equal(authorizedSenders[i], newSenders[i]) - } - }) - - it('emits an event on the Operator', async () => { - assert.equal(receipt.events?.length, 1) - - const encodedSenders1 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'address'], - [newSenders, await roles.defaultAccount.getAddress()], - ) - - const responseEvent1 = receipt.events?.[0] - assert.equal(responseEvent1?.event, 'AuthorizedSendersChanged') - assert.equal(responseEvent1?.data, encodedSenders1) - }) - - it('replaces the authorized nodes', async () => { - const originalAuthorization = await operator - .connect(roles.defaultAccount) - .isAuthorizedSender(await roles.oracleNode.getAddress()) - assert.isFalse(originalAuthorization) - }) - - after(async () => { - await operator - .connect(roles.defaultAccount) - .setAuthorizedSenders([await roles.oracleNode.getAddress()]) - }) - }) - - describe('setting 0 authorized senders', () => { - beforeEach(async () => { - newSenders = [] - }) - - it('reverts with a minimum senders message', async () => { - await evmRevert( - operator - .connect(roles.defaultAccount) - .setAuthorizedSenders(newSenders), - 'Must have at least 1 sender', - ) - }) - }) - }) - - describe('when called by an authorized sender', () => { - beforeEach(async () => { - newSenders = [await roles.oracleNode1.getAddress()] - await operator - .connect(roles.defaultAccount) - .setAuthorizedSenders(newSenders) - }) - - it('succeeds', async () => { - await operator - .connect(roles.defaultAccount) - .setAuthorizedSenders([await roles.stranger.getAddress()]) - }) - }) - - describe('when called by a non-owner', () => { - it('cannot add an authorized node', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .setAuthorizedSenders([await roles.stranger.getAddress()]), - 'Cannot set authorized senders', - ) - }) - }) - }) - - describe('#setAuthorizedSendersOn', () => { - let newSenders: string[] - - beforeEach(async () => { - await operator - .connect(roles.defaultAccount) - .setAuthorizedSenders([await roles.oracleNode1.getAddress()]) - newSenders = [ - await roles.oracleNode2.getAddress(), - await roles.oracleNode3.getAddress(), - ] - - forwarder1 = await forwarderFactory - .connect(owner) - .deploy(link.address, operator.address, zeroAddress, '0x') - forwarder2 = await forwarderFactory - .connect(owner) - .deploy(link.address, operator.address, zeroAddress, '0x') - }) - - describe('when called by a non-authorized sender', () => { - it('reverts', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .setAuthorizedSendersOn(newSenders, [forwarder1.address]), - 'Cannot set authorized senders', - ) - }) - }) - - describe('when called by an owner', () => { - it('does not revert', async () => { - await operator - .connect(roles.defaultAccount) - .setAuthorizedSendersOn( - [forwarder1.address, forwarder2.address], - newSenders, - ) - }) - }) - - describe('when called by an authorized sender', () => { - it('does not revert', async () => { - await operator - .connect(roles.oracleNode1) - .setAuthorizedSendersOn( - [forwarder1.address, forwarder2.address], - newSenders, - ) - }) - - it('does revert with 0 senders', async () => { - await operator - .connect(roles.oracleNode1) - .setAuthorizedSendersOn( - [forwarder1.address, forwarder2.address], - newSenders, - ) - }) - - it('emits a log announcing the change and who made it', async () => { - const targets = [forwarder1.address, forwarder2.address] - const tx = await operator - .connect(roles.oracleNode1) - .setAuthorizedSendersOn(targets, newSenders) - - const receipt = await tx.wait() - const encodedArgs = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'address[]', 'address'], - [targets, newSenders, await roles.oracleNode1.getAddress()], - ) - - const event1 = receipt.events?.[0] - assert.equal(event1?.event, 'TargetsUpdatedAuthorizedSenders') - assert.equal(event1?.address, operator.address) - assert.equal(event1?.data, encodedArgs) - }) - - it('updates the sender list on each of the targets', async () => { - const tx = await operator - .connect(roles.oracleNode1) - .setAuthorizedSendersOn( - [forwarder1.address, forwarder2.address], - newSenders, - ) - - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 3, receipt.toString()) - const encodedSenders = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'address'], - [newSenders, operator.address], - ) - - const event1 = receipt.events?.[1] - assert.equal(event1?.event, 'AuthorizedSendersChanged') - assert.equal(event1?.address, forwarder1.address) - assert.equal(event1?.data, encodedSenders) - - const event2 = receipt.events?.[2] - assert.equal(event2?.event, 'AuthorizedSendersChanged') - assert.equal(event2?.address, forwarder2.address) - assert.equal(event2?.data, encodedSenders) - }) - }) - }) - - describe('#acceptAuthorizedReceivers', () => { - let newSenders: string[] - - describe('being called by the owner', () => { - let operator2: Contract - let receipt: ContractReceipt - - beforeEach(async () => { - operator2 = await operatorFactory - .connect(roles.defaultAccount) - .deploy(link.address, await roles.defaultAccount.getAddress()) - forwarder1 = await forwarderFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, zeroAddress, '0x') - forwarder2 = await forwarderFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, zeroAddress, '0x') - await operator - .connect(roles.defaultAccount) - .transferOwnableContracts( - [forwarder1.address, forwarder2.address], - operator2.address, - ) - newSenders = [ - await roles.oracleNode2.getAddress(), - await roles.oracleNode3.getAddress(), - ] - - const tx = await operator2 - .connect(roles.defaultAccount) - .acceptAuthorizedReceivers( - [forwarder1.address, forwarder2.address], - newSenders, - ) - receipt = await tx.wait() - }) - - it('sets the new owner on the forwarder', async () => { - assert.equal(await forwarder1.owner(), operator2.address) - }) - - it('emits ownership transferred events', async () => { - assert.equal(receipt?.events?.[0]?.event, 'OwnableContractAccepted') - assert.equal(receipt?.events?.[0]?.args?.[0], forwarder1.address) - - assert.equal(receipt?.events?.[1]?.event, 'OwnershipTransferred') - assert.equal(receipt?.events?.[1]?.address, forwarder1.address) - assert.equal(receipt?.events?.[1]?.args?.[0], operator.address) - assert.equal(receipt?.events?.[1]?.args?.[1], operator2.address) - - assert.equal(receipt?.events?.[2]?.event, 'OwnableContractAccepted') - assert.equal(receipt?.events?.[2]?.args?.[0], forwarder2.address) - - assert.equal(receipt?.events?.[3]?.event, 'OwnershipTransferred') - assert.equal(receipt?.events?.[3]?.address, forwarder2.address) - assert.equal(receipt?.events?.[3]?.args?.[0], operator.address) - assert.equal(receipt?.events?.[3]?.args?.[1], operator2.address) - - assert.equal( - receipt?.events?.[4]?.event, - 'TargetsUpdatedAuthorizedSenders', - ) - - const encodedSenders = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'address'], - [newSenders, operator2.address], - ) - assert.equal(receipt?.events?.[5]?.event, 'AuthorizedSendersChanged') - assert.equal(receipt?.events?.[5]?.address, forwarder1.address) - assert.equal(receipt?.events?.[5]?.data, encodedSenders) - - assert.equal(receipt?.events?.[6]?.event, 'AuthorizedSendersChanged') - assert.equal(receipt?.events?.[6]?.address, forwarder2.address) - assert.equal(receipt?.events?.[6]?.data, encodedSenders) - }) - }) - - describe('being called by a non owner', () => { - it('reverts with message', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .acceptAuthorizedReceivers( - [forwarder1.address, forwarder2.address], - newSenders, - ), - 'Cannot set authorized senders', - ) - }) - }) - }) - - describe('#onTokenTransfer', () => { - describe('when called from any address but the LINK token', () => { - it('triggers the intended method', async () => { - const callData = encodeOracleRequest( - specId, - to, - fHash, - 0, - constants.HashZero, - ) - - await evmRevert( - operator.onTokenTransfer( - await roles.defaultAccount.getAddress(), - 0, - callData, - ), - ) - }) - }) - - describe('when called from the LINK token', () => { - it('triggers the intended method', async () => { - const callData = encodeOracleRequest( - specId, - to, - fHash, - 0, - constants.HashZero, - ) - - const tx = await link.transferAndCall(operator.address, 0, callData, { - value: 0, - }) - const receipt = await tx.wait() - - assert.equal(3, receipt.logs?.length) - }) - - describe('with no data', () => { - it('reverts', async () => { - await evmRevert( - link.transferAndCall(operator.address, 0, '0x', { - value: 0, - }), - ) - }) - }) - }) - - describe('malicious requester', () => { - let mock: Contract - let requester: Contract - const paymentAmount = toWei('1') - - beforeEach(async () => { - mock = await maliciousRequesterFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(mock.address, paymentAmount) - }) - - it('cannot withdraw from oracle', async () => { - const operatorOriginalBalance = await link.balanceOf(operator.address) - const mockOriginalBalance = await link.balanceOf(mock.address) - - await evmRevert(mock.maliciousWithdraw()) - - const operatorNewBalance = await link.balanceOf(operator.address) - const mockNewBalance = await link.balanceOf(mock.address) - - bigNumEquals(operatorOriginalBalance, operatorNewBalance) - bigNumEquals(mockNewBalance, mockOriginalBalance) - }) - - describe('if the requester tries to create a requestId for another contract', () => { - it('the requesters ID will not match with the oracle contract', async () => { - const tx = await mock.maliciousTargetConsumer(to) - const receipt = await tx.wait() - - const mockRequestId = receipt.logs?.[0].data - const requestId = (receipt.events?.[0].args as any).requestId - assert.notEqual(mockRequestId, requestId) - }) - - it('the target requester can still create valid requests', async () => { - requester = await basicConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - await link.transfer(requester.address, paymentAmount) - await mock.maliciousTargetConsumer(requester.address) - await requester.requestEthereumPrice('USD', paymentAmount) - }) - }) - }) - - it('does not allow recursive calls of onTokenTransfer', async () => { - const requestPayload = encodeOracleRequest( - specId, - to, - fHash, - 0, - constants.HashZero, - ) - - const ottSelector = - operatorFactory.interface.getSighash('onTokenTransfer') - const header = - '000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef' + // to - '0000000000000000000000000000000000000000000000000000000000000539' + // amount - '0000000000000000000000000000000000000000000000000000000000000060' + // offset - '0000000000000000000000000000000000000000000000000000000000000136' // length - - const maliciousPayload = ottSelector + header + requestPayload.slice(2) - - await evmRevert( - link.transferAndCall(operator.address, 0, maliciousPayload, { - value: 0, - }), - ) - }) - }) - - describe('#oracleRequest', () => { - describe('when called through the LINK token', () => { - const paid = 100 - let log: providers.Log | undefined - let receipt: providers.TransactionReceipt - - beforeEach(async () => { - const args = encodeOracleRequest( - specId, - to, - fHash, - 1, - constants.HashZero, - ) - const tx = await link.transferAndCall(operator.address, paid, args) - receipt = await tx.wait() - assert.equal(3, receipt?.logs?.length) - - log = receipt.logs && receipt.logs[2] - }) - - it('logs an event', async () => { - assert.equal(operator.address, log?.address) - - assert.equal(log?.topics?.[1], specId) - - const req = decodeRunRequest(receipt?.logs?.[2]) - assert.equal(await roles.defaultAccount.getAddress(), req.requester) - bigNumEquals(paid, req.payment) - }) - - it('uses the expected event signature', async () => { - // If updating this test, be sure to update models.RunLogTopic. - const eventSignature = - '0xd8d7ecc4800d25fa53ce0372f13a416d98907a7ef3d8d3bdd79cf4fe75529c65' - assert.equal(eventSignature, log?.topics?.[0]) - }) - - it('does not allow the same requestId to be used twice', async () => { - const args2 = encodeOracleRequest( - specId, - to, - fHash, - 1, - constants.HashZero, - ) - await evmRevert(link.transferAndCall(operator.address, paid, args2)) - }) - - describe('when called with a payload less than 2 EVM words + function selector', () => { - it('throws an error', async () => { - const funcSelector = - operatorFactory.interface.getSighash('oracleRequest') - const maliciousData = - funcSelector + - '0000000000000000000000000000000000000000000000000000000000000000000' - await evmRevert( - link.transferAndCall(operator.address, paid, maliciousData), - ) - }) - }) - - describe('when called with a payload between 3 and 9 EVM words', () => { - it('throws an error', async () => { - const funcSelector = - operatorFactory.interface.getSighash('oracleRequest') - const maliciousData = - funcSelector + - '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001' - await evmRevert( - link.transferAndCall(operator.address, paid, maliciousData), - ) - }) - }) - }) - - describe('when dataVersion is higher than 255', () => { - it('throws an error', async () => { - const paid = 100 - const args = encodeOracleRequest( - specId, - to, - fHash, - 1, - constants.HashZero, - 256, - ) - await evmRevert(link.transferAndCall(operator.address, paid, args)) - }) - }) - - describe('when not called through the LINK token', () => { - it('reverts', async () => { - await evmRevert( - operator - .connect(roles.oracleNode) - .oracleRequest( - '0x0000000000000000000000000000000000000000', - 0, - specId, - to, - fHash, - 1, - 1, - '0x', - ), - ) - }) - }) - }) - - describe('#operatorRequest', () => { - describe('when called through the LINK token', () => { - const paid = 100 - let log: providers.Log | undefined - let receipt: providers.TransactionReceipt - - beforeEach(async () => { - const args = encodeRequestOracleData( - specId, - fHash, - 1, - constants.HashZero, - ) - const tx = await link.transferAndCall(operator.address, paid, args) - receipt = await tx.wait() - assert.equal(3, receipt?.logs?.length) - - log = receipt.logs && receipt.logs[2] - }) - - it('logs an event', async () => { - assert.equal(operator.address, log?.address) - - assert.equal(log?.topics?.[1], specId) - - const req = decodeRunRequest(receipt?.logs?.[2]) - assert.equal(await roles.defaultAccount.getAddress(), req.requester) - bigNumEquals(paid, req.payment) - }) - - it('uses the expected event signature', async () => { - // If updating this test, be sure to update models.RunLogTopic. - const eventSignature = - '0xd8d7ecc4800d25fa53ce0372f13a416d98907a7ef3d8d3bdd79cf4fe75529c65' - assert.equal(eventSignature, log?.topics?.[0]) - }) - - it('does not allow the same requestId to be used twice', async () => { - const args2 = encodeRequestOracleData( - specId, - fHash, - 1, - constants.HashZero, - ) - await evmRevert(link.transferAndCall(operator.address, paid, args2)) - }) - - describe('when called with a payload less than 2 EVM words + function selector', () => { - it('throws an error', async () => { - const funcSelector = - operatorFactory.interface.getSighash('oracleRequest') - const maliciousData = - funcSelector + - '0000000000000000000000000000000000000000000000000000000000000000000' - await evmRevert( - link.transferAndCall(operator.address, paid, maliciousData), - ) - }) - }) - - describe('when called with a payload between 3 and 9 EVM words', () => { - it('throws an error', async () => { - const funcSelector = - operatorFactory.interface.getSighash('oracleRequest') - const maliciousData = - funcSelector + - '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001' - await evmRevert( - link.transferAndCall(operator.address, paid, maliciousData), - ) - }) - }) - }) - - describe('when dataVersion is higher than 255', () => { - it('throws an error', async () => { - const paid = 100 - const args = encodeRequestOracleData( - specId, - fHash, - 1, - constants.HashZero, - 256, - ) - await evmRevert(link.transferAndCall(operator.address, paid, args)) - }) - }) - - describe('when not called through the LINK token', () => { - it('reverts', async () => { - await evmRevert( - operator - .connect(roles.oracleNode) - .oracleRequest( - '0x0000000000000000000000000000000000000000', - 0, - specId, - to, - fHash, - 1, - 1, - '0x', - ), - ) - }) - }) - }) - - describe('#fulfillOracleRequest', () => { - const response = 'Hi Mom!' - let maliciousRequester: Contract - let basicConsumer: Contract - let maliciousConsumer: Contract - let gasGuzzlingConsumer: Contract - let request: ReturnType - - describe('gas guzzling consumer [ @skip-coverage ]', () => { - beforeEach(async () => { - gasGuzzlingConsumer = await gasGuzzlingConsumerFactory - .connect(roles.consumer) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(gasGuzzlingConsumer.address, paymentAmount) - const tx = - await gasGuzzlingConsumer.gassyRequestEthereumPrice(paymentAmount) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('emits an OracleResponse event', async () => { - const fulfillParams = convertFufillParams(request, response) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 1) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - }) - - describe('cooperative consumer', () => { - beforeEach(async () => { - basicConsumer = await basicConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(basicConsumer.address, paymentAmount) - const currency = 'USD' - const tx = await basicConsumer.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - describe('when called by an unauthorized node', () => { - beforeEach(async () => { - assert.equal( - false, - await operator.isAuthorizedSender( - await roles.stranger.getAddress(), - ), - ) - }) - - it('raises an error', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .fulfillOracleRequest(...convertFufillParams(request, response)), - ) - }) - }) - - describe('when fulfilled with the wrong function', () => { - let v7Consumer - beforeEach(async () => { - v7Consumer = await v7ConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(v7Consumer.address, paymentAmount) - const currency = 'USD' - const tx = await v7Consumer.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('raises an error', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .fulfillOracleRequest(...convertFufillParams(request, response)), - ) - }) - }) - - describe('when called by an authorized node', () => { - it('raises an error if the request ID does not exist', async () => { - request.requestId = ethers.utils.formatBytes32String('DOESNOTEXIST') - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)), - ) - }) - - it('sets the value on the requested contract', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - const currentValue = await basicConsumer.currentPrice() - assert.equal(response, ethers.utils.parseBytes32String(currentValue)) - }) - - it('emits an OracleResponse event', async () => { - const fulfillParams = convertFufillParams(request, response) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 3) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - - it('does not allow a request to be fulfilled twice', async () => { - const response2 = response + ' && Hello World!!' - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response2)), - ) - - const currentValue = await basicConsumer.currentPrice() - assert.equal(response, ethers.utils.parseBytes32String(currentValue)) - }) - }) - - describe('when the oracle does not provide enough gas', () => { - // if updating this defaultGasLimit, be sure it matches with the - // defaultGasLimit specified in store/tx_manager.go - const defaultGasLimit = 500000 - - beforeEach(async () => { - bigNumEquals(0, await operator.withdrawable()) - }) - - it('does not allow the oracle to withdraw the payment', async () => { - await evmRevert( - operator.connect(roles.oracleNode).fulfillOracleRequest( - ...convertFufillParams(request, response, { - gasLimit: 70000, - }), - ), - ) - - bigNumEquals(0, await operator.withdrawable()) - }) - - it(`${defaultGasLimit} is enough to pass the gas requirement`, async () => { - await operator.connect(roles.oracleNode).fulfillOracleRequest( - ...convertFufillParams(request, response, { - gasLimit: defaultGasLimit, - }), - ) - - bigNumEquals(request.payment, await operator.withdrawable()) - }) - }) - }) - - describe('with a malicious requester', () => { - beforeEach(async () => { - const paymentAmount = toWei('1') - maliciousRequester = await maliciousRequesterFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousRequester.address, paymentAmount) - }) - - it('cannot cancel before the expiration', async () => { - await evmRevert( - maliciousRequester.maliciousRequestCancel( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ), - ) - }) - - it('cannot call functions on the LINK token through callbacks', async () => { - await evmRevert( - maliciousRequester.request( - specId, - link.address, - ethers.utils.toUtf8Bytes('transfer(address,uint256)'), - ), - ) - }) - - describe('requester lies about amount of LINK sent', () => { - it('the oracle uses the amount of LINK actually paid', async () => { - const tx = await maliciousRequester.maliciousPrice(specId) - const receipt = await tx.wait() - const req = decodeRunRequest(receipt.logs?.[3]) - - assert(toWei('1').eq(req.payment)) - }) - }) - }) - - describe('with a malicious consumer', () => { - const paymentAmount = toWei('1') - - beforeEach(async () => { - maliciousConsumer = await maliciousConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousConsumer.address, paymentAmount) - }) - - describe('fails during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('assertFail(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response2)), - ) - }) - }) - - describe('calls selfdestruct', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - await maliciousConsumer.remove() - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - }) - - describe('request is canceled during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('cancelRequestOnFulfill(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - bigNumEquals(0, await link.balanceOf(maliciousConsumer.address)) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - const mockBalance = await link.balanceOf(maliciousConsumer.address) - bigNumEquals(mockBalance, 0) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response2)), - ) - }) - }) - - describe('tries to steal funds from node', () => { - it('is not successful with call', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthCall(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with send', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthSend(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with transfer', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthTransfer(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, response)) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - }) - - describe('when calling an owned contract', () => { - beforeEach(async () => { - forwarder1 = await forwarderFactory - .connect(roles.defaultAccount) - .deploy(link.address, link.address, operator.address, '0x') - }) - - it('does not allow the contract to callback to owned contracts', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('whatever(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - let request = decodeRunRequest(receipt.logs?.[3]) - let responseParams = convertFufillParams(request, response) - // set the params to be the owned address - responseParams[2] = forwarder1.address - - //accept ownership - await operator - .connect(roles.defaultAccount) - .acceptOwnableContracts([forwarder1.address]) - - // do the thing - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...responseParams), - 'Cannot call owned contract', - ) - - await operator - .connect(roles.defaultAccount) - .transferOwnableContracts([forwarder1.address], link.address) - //reverts for a different reason after transferring ownership - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...responseParams), - 'Params do not match request ID', - ) - }) - }) - }) - }) - - describe('#fulfillOracleRequest2', () => { - describe('single word fulfils', () => { - const response = 'Hi mom!' - const responseTypes = ['bytes32'] - const responseValues = [toBytes32String(response)] - let maliciousRequester: Contract - let basicConsumer: Contract - let maliciousConsumer: Contract - let gasGuzzlingConsumer: Contract - let request: ReturnType - let request2: ReturnType - - describe('gas guzzling consumer [ @skip-coverage ]', () => { - beforeEach(async () => { - gasGuzzlingConsumer = await gasGuzzlingConsumerFactory - .connect(roles.consumer) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(gasGuzzlingConsumer.address, paymentAmount) - const tx = - await gasGuzzlingConsumer.gassyRequestEthereumPrice(paymentAmount) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('emits an OracleResponse2 event', async () => { - const fulfillParams = convertFulfill2Params( - request, - responseTypes, - responseValues, - ) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 1) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - }) - - describe('cooperative consumer', () => { - beforeEach(async () => { - basicConsumer = await basicConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(basicConsumer.address, paymentAmount) - const currency = 'USD' - const tx = await basicConsumer.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - describe('when called by an unauthorized node', () => { - beforeEach(async () => { - assert.equal( - false, - await operator.isAuthorizedSender( - await roles.stranger.getAddress(), - ), - ) - }) - - it('raises an error', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ), - ) - }) - }) - - describe('when called by an authorized node', () => { - it('raises an error if the request ID does not exist', async () => { - request.requestId = ethers.utils.formatBytes32String('DOESNOTEXIST') - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ), - ) - }) - - it('sets the value on the requested contract', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const currentValue = await basicConsumer.currentPrice() - assert.equal( - response, - ethers.utils.parseBytes32String(currentValue), - ) - }) - - it('emits an OracleResponse2 event', async () => { - const fulfillParams = convertFulfill2Params( - request, - responseTypes, - responseValues, - ) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 3) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - - it('does not allow a request to be fulfilled twice', async () => { - const response2 = response + ' && Hello World!!' - const response2Values = [toBytes32String(response2)] - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - response2Values, - ), - ), - ) - - const currentValue = await basicConsumer.currentPrice() - assert.equal( - response, - ethers.utils.parseBytes32String(currentValue), - ) - }) - }) - - describe('when the oracle does not provide enough gas', () => { - // if updating this defaultGasLimit, be sure it matches with the - // defaultGasLimit specified in store/tx_manager.go - const defaultGasLimit = 500000 - - beforeEach(async () => { - bigNumEquals(0, await operator.withdrawable()) - }) - - it('does not allow the oracle to withdraw the payment', async () => { - await evmRevert( - operator.connect(roles.oracleNode).fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - { - gasLimit: 70000, - }, - ), - ), - ) - - bigNumEquals(0, await operator.withdrawable()) - }) - - it(`${defaultGasLimit} is enough to pass the gas requirement`, async () => { - await operator.connect(roles.oracleNode).fulfillOracleRequest2( - ...convertFulfill2Params(request, responseTypes, responseValues, { - gasLimit: defaultGasLimit, - }), - ) - - bigNumEquals(request.payment, await operator.withdrawable()) - }) - }) - }) - - describe('with a malicious oracle', () => { - beforeEach(async () => { - // Setup Request 1 - basicConsumer = await basicConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(basicConsumer.address, paymentAmount) - const currency = 'USD' - const tx = await basicConsumer.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - // Setup Request 2 - await link.transfer(basicConsumer.address, paymentAmount) - const tx2 = await basicConsumer.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt2 = await tx2.wait() - request2 = decodeRunRequest(receipt2.logs?.[3]) - }) - - it('cannot spoof requestId in response data by moving calldata offset', async () => { - // Malicious Oracle Fulfill 2 - const functionSelector = '0x6ae0bc76' // fulfillOracleRequest2 - const dataOffset = - '0000000000000000000000000000000000000000000000000000000000000100' // Moved to 0x0124 - const fillerBytes = - '0000000000000000000000000000000000000000000000000000000000000000' - const expectedCalldataStart = request.requestId.slice(2) // 0xe4, this is checked against requestId in validateMultiWordResponseId - const dataSize = - '0000000000000000000000000000000000000000000000000000000000000040' // Two 32 byte blocks - const maliciousCalldataId = request2.requestId.slice(2) // 0x0124, set to a different requestId - const calldataData = - '1122334455667788991122334455667788991122334455667788991122334455' // some garbage value as response value - - const data = - functionSelector + - /** Input Params - slice off 0x prefix and pad with 0's */ - request.requestId.slice(2) + - request.payment.slice(2).padStart(64, '0') + - request.callbackAddr.slice(2).padStart(64, '0') + - request.callbackFunc.slice(2).padEnd(64, '0') + - request.expiration.slice(2).padStart(64, '0') + - // calldata "data" - dataOffset + - fillerBytes + - expectedCalldataStart + - dataSize + - maliciousCalldataId + - calldataData - - await evmRevert( - operator.connect(roles.oracleNode).signer.sendTransaction({ - to: operator.address, - data, - }), - ) - }) - }) - - describe('with a malicious requester', () => { - beforeEach(async () => { - const paymentAmount = toWei('1') - maliciousRequester = await maliciousRequesterFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousRequester.address, paymentAmount) - }) - - it('cannot cancel before the expiration', async () => { - await evmRevert( - maliciousRequester.maliciousRequestCancel( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ), - ) - }) - - it('cannot call functions on the LINK token through callbacks', async () => { - await evmRevert( - maliciousRequester.request( - specId, - link.address, - ethers.utils.toUtf8Bytes('transfer(address,uint256)'), - ), - ) - }) - - describe('requester lies about amount of LINK sent', () => { - it('the oracle uses the amount of LINK actually paid', async () => { - const tx = await maliciousRequester.maliciousPrice(specId) - const receipt = await tx.wait() - const req = decodeRunRequest(receipt.logs?.[3]) - - assert(toWei('1').eq(req.payment)) - }) - }) - }) - - describe('with a malicious consumer', () => { - const paymentAmount = toWei('1') - - beforeEach(async () => { - maliciousConsumer = await maliciousConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousConsumer.address, paymentAmount) - }) - - describe('fails during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('assertFail(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - const response2Values = [toBytes32String(response2)] - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - response2Values, - ), - ), - ) - }) - }) - - describe('calls selfdestruct', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - await maliciousConsumer.remove() - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - }) - - describe('request is canceled during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes( - 'cancelRequestOnFulfill(bytes32,bytes32)', - ), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - bigNumEquals(0, await link.balanceOf(maliciousConsumer.address)) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const mockBalance = await link.balanceOf(maliciousConsumer.address) - bigNumEquals(mockBalance, 0) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - const response2Values = [toBytes32String(response2)] - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - response2Values, - ), - ), - ) - }) - }) - - describe('tries to steal funds from node', () => { - it('is not successful with call', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthCall(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with send', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthSend(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with transfer', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthTransfer(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - }) - - describe('when calling an owned contract', () => { - beforeEach(async () => { - forwarder1 = await forwarderFactory - .connect(roles.defaultAccount) - .deploy(link.address, link.address, operator.address, '0x') - }) - - it('does not allow the contract to callback to owned contracts', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('whatever(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - let request = decodeRunRequest(receipt.logs?.[3]) - let responseParams = convertFufillParams(request, response) - // set the params to be the owned address - responseParams[2] = forwarder1.address - - //accept ownership - await operator - .connect(roles.defaultAccount) - .acceptOwnableContracts([forwarder1.address]) - - // do the thing - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...responseParams), - 'Cannot call owned contract', - ) - - await operator - .connect(roles.defaultAccount) - .transferOwnableContracts([forwarder1.address], link.address) - //reverts for a different reason after transferring ownership - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...responseParams), - 'Params do not match request ID', - ) - }) - }) - }) - }) - - describe('multi word fulfils', () => { - describe('one bytes parameter', () => { - const response = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\ - Fusce euismod malesuada ligula, eget semper metus ultrices sit amet.' - const responseTypes = ['bytes'] - const responseValues = [stringToBytes(response)] - let maliciousRequester: Contract - let multiConsumer: Contract - let maliciousConsumer: Contract - let gasGuzzlingConsumer: Contract - let request: ReturnType - - describe('gas guzzling consumer [ @skip-coverage ]', () => { - beforeEach(async () => { - gasGuzzlingConsumer = await gasGuzzlingConsumerFactory - .connect(roles.consumer) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(gasGuzzlingConsumer.address, paymentAmount) - const tx = - await gasGuzzlingConsumer.gassyMultiWordRequest(paymentAmount) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('emits an OracleResponse2 event', async () => { - const fulfillParams = convertFulfill2Params( - request, - responseTypes, - responseValues, - ) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 1) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - }) - - describe('cooperative consumer', () => { - beforeEach(async () => { - multiConsumer = await multiWordConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(multiConsumer.address, paymentAmount) - const currency = 'USD' - const tx = await multiConsumer.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it("matches the consumer's request ID", async () => { - const nonce = await multiConsumer.publicGetNextRequestCount() - const tx = await multiConsumer.requestEthereumPrice('USD', 0) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - const packed = ethers.utils.solidityPack( - ['address', 'uint256'], - [multiConsumer.address, nonce], - ) - const expected = ethers.utils.keccak256(packed) - assert.equal(expected, request.requestId) - }) - - describe('when called by an unauthorized node', () => { - beforeEach(async () => { - assert.equal( - false, - await operator.isAuthorizedSender( - await roles.stranger.getAddress(), - ), - ) - }) - - it('raises an error', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ), - ) - }) - }) - - describe('when called by an authorized node', () => { - it('raises an error if the request ID does not exist', async () => { - request.requestId = - ethers.utils.formatBytes32String('DOESNOTEXIST') - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ), - ) - }) - - it('sets the value on the requested contract', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const currentValue = await multiConsumer.currentPrice() - assert.equal(response, ethers.utils.toUtf8String(currentValue)) - }) - - it('emits an OracleResponse2 event', async () => { - const fulfillParams = convertFulfill2Params( - request, - responseTypes, - responseValues, - ) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 3) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - - it('does not allow a request to be fulfilled twice', async () => { - const response2 = response + ' && Hello World!!' - const response2Values = [stringToBytes(response2)] - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - response2Values, - ), - ), - ) - - const currentValue = await multiConsumer.currentPrice() - assert.equal(response, ethers.utils.toUtf8String(currentValue)) - }) - }) - - describe('when the oracle does not provide enough gas', () => { - // if updating this defaultGasLimit, be sure it matches with the - // defaultGasLimit specified in store/tx_manager.go - const defaultGasLimit = 500000 - - beforeEach(async () => { - bigNumEquals(0, await operator.withdrawable()) - }) - - it('does not allow the oracle to withdraw the payment', async () => { - await evmRevert( - operator.connect(roles.oracleNode).fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - { - gasLimit: 70000, - }, - ), - ), - ) - - bigNumEquals(0, await operator.withdrawable()) - }) - - it(`${defaultGasLimit} is enough to pass the gas requirement`, async () => { - await operator.connect(roles.oracleNode).fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - { - gasLimit: defaultGasLimit, - }, - ), - ) - - bigNumEquals(request.payment, await operator.withdrawable()) - }) - }) - }) - - describe('with a malicious requester', () => { - beforeEach(async () => { - const paymentAmount = toWei('1') - maliciousRequester = await maliciousRequesterFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousRequester.address, paymentAmount) - }) - - it('cannot cancel before the expiration', async () => { - await evmRevert( - maliciousRequester.maliciousRequestCancel( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ), - ) - }) - - it('cannot call functions on the LINK token through callbacks', async () => { - await evmRevert( - maliciousRequester.request( - specId, - link.address, - ethers.utils.toUtf8Bytes('transfer(address,uint256)'), - ), - ) - }) - - describe('requester lies about amount of LINK sent', () => { - it('the oracle uses the amount of LINK actually paid', async () => { - const tx = await maliciousRequester.maliciousPrice(specId) - const receipt = await tx.wait() - const req = decodeRunRequest(receipt.logs?.[3]) - - assert(toWei('1').eq(req.payment)) - }) - }) - }) - - describe('with a malicious consumer', () => { - const paymentAmount = toWei('1') - - beforeEach(async () => { - maliciousConsumer = await maliciousMultiWordConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousConsumer.address, paymentAmount) - }) - - describe('fails during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('assertFail(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - const response2Values = [stringToBytes(response2)] - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - response2Values, - ), - ), - ) - }) - }) - - describe('calls selfdestruct', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - await maliciousConsumer.remove() - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - }) - - describe('request is canceled during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes( - 'cancelRequestOnFulfill(bytes32,bytes32)', - ), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - bigNumEquals(0, await link.balanceOf(maliciousConsumer.address)) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const mockBalance = await link.balanceOf( - maliciousConsumer.address, - ) - bigNumEquals(mockBalance, 0) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response2 = 'hack the planet 102' - const response2Values = [stringToBytes(response2)] - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - response2Values, - ), - ), - ) - }) - }) - - describe('tries to steal funds from node', () => { - it('is not successful with call', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthCall(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with send', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthSend(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with transfer', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthTransfer(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - }) - }) - }) - - describe('multiple bytes32 parameters', () => { - const response1 = '100' - const response2 = '7777777' - const response3 = 'forty two' - const responseTypes = ['bytes32', 'bytes32', 'bytes32'] - const responseValues = [ - toBytes32String(response1), - toBytes32String(response2), - toBytes32String(response3), - ] - let maliciousRequester: Contract - let multiConsumer: Contract - let maliciousConsumer: Contract - let gasGuzzlingConsumer: Contract - let request: ReturnType - - describe('gas guzzling consumer [ @skip-coverage ]', () => { - beforeEach(async () => { - gasGuzzlingConsumer = await gasGuzzlingConsumerFactory - .connect(roles.consumer) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(gasGuzzlingConsumer.address, paymentAmount) - const tx = - await gasGuzzlingConsumer.gassyMultiWordRequest(paymentAmount) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('emits an OracleResponse2 event', async () => { - const fulfillParams = convertFulfill2Params( - request, - responseTypes, - responseValues, - ) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 1) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - }) - - describe('cooperative consumer', () => { - beforeEach(async () => { - multiConsumer = await multiWordConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(multiConsumer.address, paymentAmount) - const currency = 'USD' - const tx = await multiConsumer.requestMultipleParameters( - currency, - paymentAmount, - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - describe('when called by an unauthorized node', () => { - beforeEach(async () => { - assert.equal( - false, - await operator.isAuthorizedSender( - await roles.stranger.getAddress(), - ), - ) - }) - - it('raises an error', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ), - ) - }) - }) - - describe('when called by an authorized node', () => { - it('raises an error if the request ID does not exist', async () => { - request.requestId = - ethers.utils.formatBytes32String('DOESNOTEXIST') - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ), - ) - }) - - it('sets the value on the requested contract', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const firstValue = await multiConsumer.usd() - const secondValue = await multiConsumer.eur() - const thirdValue = await multiConsumer.jpy() - assert.equal( - response1, - ethers.utils.parseBytes32String(firstValue), - ) - assert.equal( - response2, - ethers.utils.parseBytes32String(secondValue), - ) - assert.equal( - response3, - ethers.utils.parseBytes32String(thirdValue), - ) - }) - - it('emits an OracleResponse2 event', async () => { - const fulfillParams = convertFulfill2Params( - request, - responseTypes, - responseValues, - ) - const tx = await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...fulfillParams) - const receipt = await tx.wait() - assert.equal(receipt.events?.length, 3) - const responseEvent = receipt.events?.[0] - assert.equal(responseEvent?.event, 'OracleResponse') - assert.equal(responseEvent?.args?.[0], request.requestId) - }) - - it('does not allow a request to be fulfilled twice', async () => { - const response4 = response3 + ' && Hello World!!' - const repeatedResponseValues = [ - toBytes32String(response1), - toBytes32String(response2), - toBytes32String(response4), - ] - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - repeatedResponseValues, - ), - ), - ) - - const firstValue = await multiConsumer.usd() - const secondValue = await multiConsumer.eur() - const thirdValue = await multiConsumer.jpy() - assert.equal( - response1, - ethers.utils.parseBytes32String(firstValue), - ) - assert.equal( - response2, - ethers.utils.parseBytes32String(secondValue), - ) - assert.equal( - response3, - ethers.utils.parseBytes32String(thirdValue), - ) - }) - }) - - describe('when the oracle does not provide enough gas', () => { - // if updating this defaultGasLimit, be sure it matches with the - // defaultGasLimit specified in store/tx_manager.go - const defaultGasLimit = 500000 - - beforeEach(async () => { - bigNumEquals(0, await operator.withdrawable()) - }) - - it('does not allow the oracle to withdraw the payment', async () => { - await evmRevert( - operator.connect(roles.oracleNode).fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - { - gasLimit: 70000, - }, - ), - ), - ) - - bigNumEquals(0, await operator.withdrawable()) - }) - - it(`${defaultGasLimit} is enough to pass the gas requirement`, async () => { - await operator.connect(roles.oracleNode).fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - { - gasLimit: defaultGasLimit, - }, - ), - ) - - bigNumEquals(request.payment, await operator.withdrawable()) - }) - }) - }) - - describe('with a malicious requester', () => { - beforeEach(async () => { - const paymentAmount = toWei('1') - maliciousRequester = await maliciousRequesterFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousRequester.address, paymentAmount) - }) - - it('cannot cancel before the expiration', async () => { - await evmRevert( - maliciousRequester.maliciousRequestCancel( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ), - ) - }) - - it('cannot call functions on the LINK token through callbacks', async () => { - await evmRevert( - maliciousRequester.request( - specId, - link.address, - ethers.utils.toUtf8Bytes('transfer(address,uint256)'), - ), - ) - }) - - describe('requester lies about amount of LINK sent', () => { - it('the oracle uses the amount of LINK actually paid', async () => { - const tx = await maliciousRequester.maliciousPrice(specId) - const receipt = await tx.wait() - const req = decodeRunRequest(receipt.logs?.[3]) - - assert(toWei('1').eq(req.payment)) - }) - }) - }) - - describe('with a malicious consumer', () => { - const paymentAmount = toWei('1') - - beforeEach(async () => { - maliciousConsumer = await maliciousMultiWordConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address) - await link.transfer(maliciousConsumer.address, paymentAmount) - }) - - describe('fails during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('assertFail(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response4 = 'hack the planet 102' - const repeatedResponseValues = [ - toBytes32String(response1), - toBytes32String(response2), - toBytes32String(response4), - ] - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - repeatedResponseValues, - ), - ), - ) - }) - }) - - describe('calls selfdestruct', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('doesNothing(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - await maliciousConsumer.remove() - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - }) - - describe('request is canceled during fulfillment', () => { - beforeEach(async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes( - 'cancelRequestOnFulfill(bytes32,bytes32)', - ), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - bigNumEquals(0, await link.balanceOf(maliciousConsumer.address)) - }) - - it('allows the oracle node to receive their payment', async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - const mockBalance = await link.balanceOf( - maliciousConsumer.address, - ) - bigNumEquals(mockBalance, 0) - - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(balance, 0) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), paymentAmount) - const newBalance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - bigNumEquals(paymentAmount, newBalance) - }) - - it("can't fulfill the data again", async () => { - const response4 = 'hack the planet 102' - const repeatedResponseValues = [ - toBytes32String(response1), - toBytes32String(response2), - toBytes32String(response4), - ] - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - repeatedResponseValues, - ), - ), - ) - }) - }) - - describe('tries to steal funds from node', () => { - it('is not successful with call', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthCall(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with send', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthSend(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - - it('is not successful with transfer', async () => { - const tx = await maliciousConsumer.requestData( - specId, - ethers.utils.toUtf8Bytes('stealEthTransfer(bytes32,bytes32)'), - ) - const receipt = await tx.wait() - request = decodeRunRequest(receipt.logs?.[3]) - - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params( - request, - responseTypes, - responseValues, - ), - ) - bigNumEquals( - 0, - await ethers.provider.getBalance(maliciousConsumer.address), - ) - }) - }) - }) - }) - }) - - describe('when the response data is too short', () => { - const response = 'Hi mom!' - const responseTypes = ['bytes32'] - const responseValues = [toBytes32String(response)] - - it('reverts', async () => { - let basicConsumer = await basicConsumerFactory - .connect(roles.defaultAccount) - .deploy(link.address, operator.address, specId) - const paymentAmount = toWei('1') - await link.transfer(basicConsumer.address, paymentAmount) - const tx = await basicConsumer.requestEthereumPrice( - 'USD', - paymentAmount, - ) - const receipt = await tx.wait() - let request = decodeRunRequest(receipt.logs?.[3]) - - const fulfillParams = convertFulfill2Params( - request, - responseTypes, - responseValues, - ) - fulfillParams[5] = '0x' // overwrite the data to be of lenght 0 - await evmRevert( - operator - .connect(roles.oracleNode) - .fulfillOracleRequest2(...fulfillParams), - 'Response must be > 32 bytes', - ) - }) - }) - }) - - describe('#withdraw', () => { - describe('without reserving funds via oracleRequest', () => { - it('does nothing', async () => { - let balance = await link.balanceOf(await roles.oracleNode.getAddress()) - assert.equal(0, balance.toNumber()) - await evmRevert( - operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), toWei('1')), - ) - balance = await link.balanceOf(await roles.oracleNode.getAddress()) - assert.equal(0, balance.toNumber()) - }) - - describe('recovering funds that were mistakenly sent', () => { - const paid = 1 - beforeEach(async () => { - await link.transfer(operator.address, paid) - }) - - it('withdraws funds', async () => { - const operatorBalanceBefore = await link.balanceOf(operator.address) - const accountBalanceBefore = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.defaultAccount.getAddress(), paid) - - const operatorBalanceAfter = await link.balanceOf(operator.address) - const accountBalanceAfter = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - - const accountDifference = - accountBalanceAfter.sub(accountBalanceBefore) - const operatorDifference = - operatorBalanceBefore.sub(operatorBalanceAfter) - - bigNumEquals(operatorDifference, paid) - bigNumEquals(accountDifference, paid) - }) - }) - }) - - describe('reserving funds via oracleRequest', () => { - const payment = 15 - let request: ReturnType - - beforeEach(async () => { - const requester = await roles.defaultAccount.getAddress() - const args = encodeOracleRequest( - specId, - requester, - fHash, - 0, - constants.HashZero, - ) - const tx = await link.transferAndCall(operator.address, payment, args) - const receipt = await tx.wait() - assert.equal(3, receipt.logs?.length) - request = decodeRunRequest(receipt.logs?.[2]) - }) - - describe('but not freeing funds w fulfillOracleRequest', () => { - it('does not transfer funds', async () => { - await evmRevert( - operator - .connect(roles.defaultAccount) - .withdraw(await roles.oracleNode.getAddress(), payment), - ) - const balance = await link.balanceOf( - await roles.oracleNode.getAddress(), - ) - assert.equal(0, balance.toNumber()) - }) - - describe('recovering funds that were mistakenly sent', () => { - const paid = 1 - beforeEach(async () => { - await link.transfer(operator.address, paid) - }) - - it('withdraws funds', async () => { - const operatorBalanceBefore = await link.balanceOf(operator.address) - const accountBalanceBefore = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.defaultAccount.getAddress(), paid) - - const operatorBalanceAfter = await link.balanceOf(operator.address) - const accountBalanceAfter = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - - const accountDifference = - accountBalanceAfter.sub(accountBalanceBefore) - const operatorDifference = - operatorBalanceBefore.sub(operatorBalanceAfter) - - bigNumEquals(operatorDifference, paid) - bigNumEquals(accountDifference, paid) - }) - }) - }) - - describe('and freeing funds', () => { - beforeEach(async () => { - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest( - ...convertFufillParams(request, 'Hello World!'), - ) - }) - - it('does not allow input greater than the balance', async () => { - const originalOracleBalance = await link.balanceOf(operator.address) - const originalStrangerBalance = await link.balanceOf( - await roles.stranger.getAddress(), - ) - const withdrawalAmount = payment + 1 - - assert.isAbove(withdrawalAmount, originalOracleBalance.toNumber()) - await evmRevert( - operator - .connect(roles.defaultAccount) - .withdraw(await roles.stranger.getAddress(), withdrawalAmount), - ) - - const newOracleBalance = await link.balanceOf(operator.address) - const newStrangerBalance = await link.balanceOf( - await roles.stranger.getAddress(), - ) - - assert.equal( - originalOracleBalance.toNumber(), - newOracleBalance.toNumber(), - ) - assert.equal( - originalStrangerBalance.toNumber(), - newStrangerBalance.toNumber(), - ) - }) - - it('allows transfer of partial balance by owner to specified address', async () => { - const partialAmount = 6 - const difference = payment - partialAmount - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.stranger.getAddress(), partialAmount) - const strangerBalance = await link.balanceOf( - await roles.stranger.getAddress(), - ) - const oracleBalance = await link.balanceOf(operator.address) - assert.equal(partialAmount, strangerBalance.toNumber()) - assert.equal(difference, oracleBalance.toNumber()) - }) - - it('allows transfer of entire balance by owner to specified address', async () => { - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.stranger.getAddress(), payment) - const balance = await link.balanceOf( - await roles.stranger.getAddress(), - ) - assert.equal(payment, balance.toNumber()) - }) - - it('does not allow a transfer of funds by non-owner', async () => { - await evmRevert( - operator - .connect(roles.stranger) - .withdraw(await roles.stranger.getAddress(), payment), - ) - const balance = await link.balanceOf( - await roles.stranger.getAddress(), - ) - assert.isTrue(ethers.constants.Zero.eq(balance)) - }) - - describe('recovering funds that were mistakenly sent', () => { - const paid = 1 - beforeEach(async () => { - await link.transfer(operator.address, paid) - }) - - it('withdraws funds', async () => { - const operatorBalanceBefore = await link.balanceOf(operator.address) - const accountBalanceBefore = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - - await operator - .connect(roles.defaultAccount) - .withdraw(await roles.defaultAccount.getAddress(), paid) - - const operatorBalanceAfter = await link.balanceOf(operator.address) - const accountBalanceAfter = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - - const accountDifference = - accountBalanceAfter.sub(accountBalanceBefore) - const operatorDifference = - operatorBalanceBefore.sub(operatorBalanceAfter) - - bigNumEquals(operatorDifference, paid) - bigNumEquals(accountDifference, paid) - }) - }) - }) - }) - }) - - describe('#withdrawable', () => { - let request: ReturnType - const amount = toWei('1') - - beforeEach(async () => { - const requester = await roles.defaultAccount.getAddress() - const args = encodeOracleRequest( - specId, - requester, - fHash, - 0, - constants.HashZero, - ) - const tx = await link.transferAndCall(operator.address, amount, args) - const receipt = await tx.wait() - assert.equal(3, receipt.logs?.length) - request = decodeRunRequest(receipt.logs?.[2]) - await operator - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request, 'Hello World!')) - }) - - it('returns the correct value', async () => { - const withdrawAmount = await operator.withdrawable() - bigNumEquals(withdrawAmount, request.payment) - }) - - describe('funds that were mistakenly sent', () => { - const paid = 1 - beforeEach(async () => { - await link.transfer(operator.address, paid) - }) - - it('returns the correct value', async () => { - const withdrawAmount = await operator.withdrawable() - - const expectedAmount = amount.add(paid) - bigNumEquals(withdrawAmount, expectedAmount) - }) - }) - }) - - describe('#ownerTransferAndCall', () => { - let operator2: Contract - let args: string - let to: string - const startingBalance = 1000 - const payment = 20 - - beforeEach(async () => { - operator2 = await operatorFactory - .connect(roles.oracleNode2) - .deploy(link.address, await roles.oracleNode2.getAddress()) - to = operator2.address - args = encodeOracleRequest( - specId, - operator.address, - operatorFactory.interface.getSighash('fulfillOracleRequest'), - 1, - constants.HashZero, - ) - }) - - describe('when called by a non-owner', () => { - it('reverts with owner error message', async () => { - await link.transfer(operator.address, startingBalance) - await evmRevert( - operator - .connect(roles.stranger) - .ownerTransferAndCall(to, payment, args), - 'Only callable by owner', - ) - }) - }) - - describe('when called by the owner', () => { - beforeEach(async () => { - await link.transfer(operator.address, startingBalance) - }) - - describe('without sufficient funds in contract', () => { - it('reverts with funds message', async () => { - const tooMuch = startingBalance * 2 - await evmRevert( - operator - .connect(roles.defaultAccount) - .ownerTransferAndCall(to, tooMuch, args), - 'Amount requested is greater than withdrawable balance', - ) - }) - }) - - describe('with sufficient funds', () => { - let tx: ContractTransaction - let receipt: ContractReceipt - let requesterBalanceBefore: BigNumber - let requesterBalanceAfter: BigNumber - let receiverBalanceBefore: BigNumber - let receiverBalanceAfter: BigNumber - - before(async () => { - requesterBalanceBefore = await link.balanceOf(operator.address) - receiverBalanceBefore = await link.balanceOf(operator2.address) - tx = await operator - .connect(roles.defaultAccount) - .ownerTransferAndCall(to, payment, args) - receipt = await tx.wait() - requesterBalanceAfter = await link.balanceOf(operator.address) - receiverBalanceAfter = await link.balanceOf(operator2.address) - }) - - it('emits an event', async () => { - assert.equal(3, receipt.logs?.length) - const transferLog = await getLog(tx, 1) - const parsedLog = link.interface.parseLog({ - data: transferLog.data, - topics: transferLog.topics, - }) - await expect(parsedLog.name).to.equal('Transfer') - }) - - it('transfers the tokens', async () => { - bigNumEquals( - requesterBalanceBefore.sub(requesterBalanceAfter), - payment, - ) - bigNumEquals(receiverBalanceAfter.sub(receiverBalanceBefore), payment) - }) - }) - }) - }) - - describe('#cancelOracleRequestByRequester', () => { - const nonce = 17 - - describe('with no pending requests', () => { - it('fails', async () => { - const fakeRequest: RunRequest = { - requestId: ethers.utils.formatBytes32String('1337'), - payment: '0', - callbackFunc: - getterSetterFactory.interface.getSighash('requestedBytes32'), - expiration: '999999999999', - - callbackAddr: '', - data: Buffer.from(''), - dataVersion: 0, - specId: '', - requester: '', - topic: '', - } - await increaseTime5Minutes(ethers.provider) - - await evmRevert( - operator - .connect(roles.stranger) - .cancelOracleRequestByRequester( - ...convertCancelByRequesterParams(fakeRequest, nonce), - ), - ) - }) - }) - - describe('with a pending request', () => { - const startingBalance = 100 - let request: ReturnType - let receipt: providers.TransactionReceipt - - beforeEach(async () => { - const requestAmount = 20 - - await link.transfer(await roles.consumer.getAddress(), startingBalance) - - const args = encodeOracleRequest( - specId, - await roles.consumer.getAddress(), - fHash, - nonce, - constants.HashZero, - ) - const tx = await link - .connect(roles.consumer) - .transferAndCall(operator.address, requestAmount, args) - receipt = await tx.wait() - - assert.equal(3, receipt.logs?.length) - request = decodeRunRequest(receipt.logs?.[2]) - - // pre conditions - const oracleBalance = await link.balanceOf(operator.address) - bigNumEquals(request.payment, oracleBalance) - - const consumerAmount = await link.balanceOf( - await roles.consumer.getAddress(), - ) - assert.equal( - startingBalance - Number(request.payment), - consumerAmount.toNumber(), - ) - }) - - describe('from a stranger', () => { - it('fails', async () => { - await evmRevert( - operator - .connect(roles.consumer) - .cancelOracleRequestByRequester( - ...convertCancelByRequesterParams(request, nonce), - ), - ) - }) - }) - - describe('from the requester', () => { - it('refunds the correct amount', async () => { - await increaseTime5Minutes(ethers.provider) - await operator - .connect(roles.consumer) - .cancelOracleRequestByRequester( - ...convertCancelByRequesterParams(request, nonce), - ) - const balance = await link.balanceOf( - await roles.consumer.getAddress(), - ) - - assert.equal(startingBalance, balance.toNumber()) // 100 - }) - - it('triggers a cancellation event', async () => { - await increaseTime5Minutes(ethers.provider) - const tx = await operator - .connect(roles.consumer) - .cancelOracleRequestByRequester( - ...convertCancelByRequesterParams(request, nonce), - ) - const receipt = await tx.wait() - - assert.equal(receipt.logs?.length, 2) - assert.equal(request.requestId, receipt.logs?.[0].topics[1]) - }) - - it('fails when called twice', async () => { - await increaseTime5Minutes(ethers.provider) - await operator - .connect(roles.consumer) - .cancelOracleRequestByRequester( - ...convertCancelByRequesterParams(request, nonce), - ) - - await evmRevert( - operator - .connect(roles.consumer) - .cancelOracleRequestByRequester(...convertCancelParams(request)), - ) - }) - }) - }) - }) - - describe('#cancelOracleRequest', () => { - describe('with no pending requests', () => { - it('fails', async () => { - const fakeRequest: RunRequest = { - requestId: ethers.utils.formatBytes32String('1337'), - payment: '0', - callbackFunc: - getterSetterFactory.interface.getSighash('requestedBytes32'), - expiration: '999999999999', - - callbackAddr: '', - data: Buffer.from(''), - dataVersion: 0, - specId: '', - requester: '', - topic: '', - } - await increaseTime5Minutes(ethers.provider) - - await evmRevert( - operator - .connect(roles.stranger) - .cancelOracleRequest(...convertCancelParams(fakeRequest)), - ) - }) - }) - - describe('with a pending request', () => { - const startingBalance = 100 - let request: ReturnType - let receipt: providers.TransactionReceipt - - beforeEach(async () => { - const requestAmount = 20 - - await link.transfer(await roles.consumer.getAddress(), startingBalance) - - const args = encodeOracleRequest( - specId, - await roles.consumer.getAddress(), - fHash, - 1, - constants.HashZero, - ) - const tx = await link - .connect(roles.consumer) - .transferAndCall(operator.address, requestAmount, args) - receipt = await tx.wait() - - assert.equal(3, receipt.logs?.length) - request = decodeRunRequest(receipt.logs?.[2]) - }) - - it('has correct initial balances', async () => { - const oracleBalance = await link.balanceOf(operator.address) - bigNumEquals(request.payment, oracleBalance) - - const consumerAmount = await link.balanceOf( - await roles.consumer.getAddress(), - ) - assert.equal( - startingBalance - Number(request.payment), - consumerAmount.toNumber(), - ) - }) - - describe('from a stranger', () => { - it('fails', async () => { - await evmRevert( - operator - .connect(roles.consumer) - .cancelOracleRequest(...convertCancelParams(request)), - ) - }) - }) - - describe('from the requester', () => { - it('refunds the correct amount', async () => { - await increaseTime5Minutes(ethers.provider) - await operator - .connect(roles.consumer) - .cancelOracleRequest(...convertCancelParams(request)) - const balance = await link.balanceOf( - await roles.consumer.getAddress(), - ) - - assert.equal(startingBalance, balance.toNumber()) // 100 - }) - - it('triggers a cancellation event', async () => { - await increaseTime5Minutes(ethers.provider) - const tx = await operator - .connect(roles.consumer) - .cancelOracleRequest(...convertCancelParams(request)) - const receipt = await tx.wait() - - assert.equal(receipt.logs?.length, 2) - assert.equal(request.requestId, receipt.logs?.[0].topics[1]) - }) - - it('fails when called twice', async () => { - await increaseTime5Minutes(ethers.provider) - await operator - .connect(roles.consumer) - .cancelOracleRequest(...convertCancelParams(request)) - - await evmRevert( - operator - .connect(roles.consumer) - .cancelOracleRequest(...convertCancelParams(request)), - ) - }) - }) - }) - }) - - describe('#ownerForward', () => { - let bytes: string - let payload: string - let mock: Contract - - beforeEach(async () => { - bytes = ethers.utils.hexlify(ethers.utils.randomBytes(100)) - payload = getterSetterFactory.interface.encodeFunctionData( - getterSetterFactory.interface.getFunction('setBytes'), - [bytes], - ) - mock = await getterSetterFactory.connect(roles.defaultAccount).deploy() - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - operator.connect(roles.stranger).ownerForward(mock.address, payload), - ) - }) - }) - - describe('when called by owner', () => { - describe('when attempting to forward to the link token', () => { - it('reverts', async () => { - const sighash = linkTokenFactory.interface.getSighash('name') - await evmRevert( - operator - .connect(roles.defaultAccount) - .ownerForward(link.address, sighash), - 'Cannot call to LINK', - ) - }) - }) - - describe('when forwarding to any other address', () => { - it('forwards the data', async () => { - const tx = await operator - .connect(roles.defaultAccount) - .ownerForward(mock.address, payload) - await tx.wait() - assert.equal(await mock.getBytes(), bytes) - }) - - it('reverts when sending to a non-contract address', async () => { - await evmRevert( - operator - .connect(roles.defaultAccount) - .ownerForward(zeroAddress, payload), - 'Must forward to a contract', - ) - }) - - it('perceives the message is sent by the Operator', async () => { - const tx = await operator - .connect(roles.defaultAccount) - .ownerForward(mock.address, payload) - const receipt = await tx.wait() - const log: any = receipt.logs?.[0] - const logData = mock.interface.decodeEventLog( - mock.interface.getEvent('SetBytes'), - log.data, - log.topics, - ) - assert.equal(ethers.utils.getAddress(logData.from), operator.address) - }) - }) - }) - }) -}) diff --git a/contracts/test/v0.7/OperatorFactory.test.ts b/contracts/test/v0.7/OperatorFactory.test.ts deleted file mode 100644 index d2a24600e23..00000000000 --- a/contracts/test/v0.7/OperatorFactory.test.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { ethers } from 'hardhat' -import { evmWordToAddress, publicAbi } from '../test-helpers/helpers' -import { assert } from 'chai' -import { Contract, ContractFactory, ContractReceipt } from 'ethers' -import { getUsers, Roles } from '../test-helpers/setup' - -let linkTokenFactory: ContractFactory -let operatorGeneratorFactory: ContractFactory -let operatorFactory: ContractFactory -let forwarderFactory: ContractFactory - -let roles: Roles - -before(async () => { - const users = await getUsers() - - roles = users.roles - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) - operatorGeneratorFactory = await ethers.getContractFactory( - 'src/v0.7/OperatorFactory.sol:OperatorFactory', - roles.defaultAccount, - ) - operatorFactory = await ethers.getContractFactory( - 'src/v0.7/Operator.sol:Operator', - roles.defaultAccount, - ) - forwarderFactory = await ethers.getContractFactory( - 'src/v0.7/AuthorizedForwarder.sol:AuthorizedForwarder', - roles.defaultAccount, - ) -}) - -describe('OperatorFactory', () => { - let link: Contract - let operatorGenerator: Contract - let operator: Contract - let forwarder: Contract - let receipt: ContractReceipt - let emittedOperator: string - let emittedForwarder: string - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - operatorGenerator = await operatorGeneratorFactory - .connect(roles.defaultAccount) - .deploy(link.address) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(operatorGenerator, [ - 'created', - 'deployNewOperator', - 'deployNewOperatorAndForwarder', - 'deployNewForwarder', - 'deployNewForwarderAndTransferOwnership', - 'getChainlinkToken', - 'typeAndVersion', - ]) - }) - - describe('#typeAndVersion', () => { - it('describes the authorized forwarder', async () => { - assert.equal( - await operatorGenerator.typeAndVersion(), - 'OperatorFactory 1.0.0', - ) - }) - }) - - describe('#deployNewOperator', () => { - beforeEach(async () => { - const tx = await operatorGenerator - .connect(roles.oracleNode) - .deployNewOperator() - - receipt = await tx.wait() - emittedOperator = evmWordToAddress(receipt.logs?.[0].topics?.[1]) - }) - - it('emits an event', async () => { - assert.equal(receipt?.events?.[0]?.event, 'OperatorCreated') - assert.equal(emittedOperator, receipt.events?.[0].args?.[0]) - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[0].args?.[1], - ) - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[0].args?.[2], - ) - }) - - it('sets the correct owner', async () => { - operator = await operatorFactory - .connect(roles.defaultAccount) - .attach(emittedOperator) - const ownerString = await operator.owner() - assert.equal(ownerString, await roles.oracleNode.getAddress()) - }) - - it('records that it deployed that address', async () => { - assert.isTrue(await operatorGenerator.created(emittedOperator)) - }) - }) - - describe('#deployNewOperatorAndForwarder', () => { - beforeEach(async () => { - const tx = await operatorGenerator - .connect(roles.oracleNode) - .deployNewOperatorAndForwarder() - - receipt = await tx.wait() - emittedOperator = evmWordToAddress(receipt.logs?.[0].topics?.[1]) - emittedForwarder = evmWordToAddress(receipt.logs?.[3].topics?.[1]) - }) - - it('emits an event recording that the operator was deployed', async () => { - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[0].args?.[1], - ) - assert.equal(receipt?.events?.[0]?.event, 'OperatorCreated') - assert.equal(receipt?.events?.[0]?.args?.[0], emittedOperator) - assert.equal( - receipt?.events?.[0]?.args?.[1], - await roles.oracleNode.getAddress(), - ) - assert.equal( - receipt?.events?.[0]?.args?.[2], - await roles.oracleNode.getAddress(), - ) - }) - - it('proposes the transfer of the forwarder to the operator', async () => { - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[0].args?.[1], - ) - assert.equal( - receipt?.events?.[1]?.topics?.[0], - '0xed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae1278', //OwnershipTransferRequested(address,address) - ) - assert.equal( - evmWordToAddress(receipt?.events?.[1]?.topics?.[1]), - operatorGenerator.address, - ) - assert.equal( - evmWordToAddress(receipt?.events?.[1]?.topics?.[2]), - emittedOperator, - ) - - assert.equal( - receipt?.events?.[2]?.topics?.[0], - '0x4e1e878dc28d5f040db5969163ff1acd75c44c3f655da2dde9c70bbd8e56dc7e', //OwnershipTransferRequestedWithMessage(address,address,bytes) - ) - assert.equal( - evmWordToAddress(receipt?.events?.[2]?.topics?.[1]), - operatorGenerator.address, - ) - assert.equal( - evmWordToAddress(receipt?.events?.[2]?.topics?.[2]), - emittedOperator, - ) - }) - - it('emits an event recording that the forwarder was deployed', async () => { - assert.equal(receipt?.events?.[3]?.event, 'AuthorizedForwarderCreated') - assert.equal(receipt?.events?.[3]?.args?.[0], emittedForwarder) - assert.equal(receipt?.events?.[3]?.args?.[1], operatorGenerator.address) - assert.equal( - receipt?.events?.[3]?.args?.[2], - await roles.oracleNode.getAddress(), - ) - }) - - it('sets the correct owner on the operator', async () => { - operator = await operatorFactory - .connect(roles.defaultAccount) - .attach(receipt?.events?.[0]?.args?.[0]) - assert.equal(await roles.oracleNode.getAddress(), await operator.owner()) - }) - - it('sets the operator as the owner of the forwarder', async () => { - forwarder = await forwarderFactory - .connect(roles.defaultAccount) - .attach(emittedForwarder) - assert.equal(operatorGenerator.address, await forwarder.owner()) - }) - - it('records that it deployed that address', async () => { - assert.isTrue(await operatorGenerator.created(emittedOperator)) - assert.isTrue(await operatorGenerator.created(emittedForwarder)) - }) - }) - - describe('#deployNewForwarder', () => { - beforeEach(async () => { - const tx = await operatorGenerator - .connect(roles.oracleNode) - .deployNewForwarder() - - receipt = await tx.wait() - emittedForwarder = receipt.events?.[0].args?.[0] - }) - - it('emits an event', async () => { - assert.equal(receipt?.events?.[0]?.event, 'AuthorizedForwarderCreated') - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[0].args?.[1], - ) // owner - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[0].args?.[2], - ) // sender - }) - - it('sets the caller as the owner', async () => { - forwarder = await forwarderFactory - .connect(roles.defaultAccount) - .attach(emittedForwarder) - const ownerString = await forwarder.owner() - assert.equal(ownerString, await roles.oracleNode.getAddress()) - }) - - it('records that it deployed that address', async () => { - assert.isTrue(await operatorGenerator.created(emittedForwarder)) - }) - }) - - describe('#deployNewForwarderAndTransferOwnership', () => { - const message = '0x42' - - beforeEach(async () => { - const tx = await operatorGenerator - .connect(roles.oracleNode) - .deployNewForwarderAndTransferOwnership( - await roles.stranger.getAddress(), - message, - ) - receipt = await tx.wait() - - emittedForwarder = evmWordToAddress(receipt.logs?.[2].topics?.[1]) - }) - - it('emits an event', async () => { - assert.equal(receipt?.events?.[2]?.event, 'AuthorizedForwarderCreated') - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[2].args?.[1], - ) // owner - assert.equal( - await roles.oracleNode.getAddress(), - receipt.events?.[2].args?.[2], - ) // sender - }) - - it('sets the caller as the owner', async () => { - forwarder = await forwarderFactory - .connect(roles.defaultAccount) - .attach(emittedForwarder) - const ownerString = await forwarder.owner() - assert.equal(ownerString, await roles.oracleNode.getAddress()) - }) - - it('proposes a transfer to the recipient', async () => { - const emittedOwner = evmWordToAddress(receipt.logs?.[0].topics?.[1]) - assert.equal(emittedOwner, await roles.oracleNode.getAddress()) - const emittedRecipient = evmWordToAddress(receipt.logs?.[0].topics?.[2]) - assert.equal(emittedRecipient, await roles.stranger.getAddress()) - }) - - it('proposes a transfer to the recipient with the specified message', async () => { - const emittedOwner = evmWordToAddress(receipt.logs?.[1].topics?.[1]) - assert.equal(emittedOwner, await roles.oracleNode.getAddress()) - const emittedRecipient = evmWordToAddress(receipt.logs?.[1].topics?.[2]) - assert.equal(emittedRecipient, await roles.stranger.getAddress()) - - const encodedMessage = ethers.utils.defaultAbiCoder.encode( - ['bytes'], - [message], - ) - assert.equal(receipt?.logs?.[1]?.data, encodedMessage) - }) - - it('records that it deployed that address', async () => { - assert.isTrue(await operatorGenerator.created(emittedForwarder)) - }) - }) -}) diff --git a/contracts/test/v0.7/StalenessFlaggingValidator.test.ts b/contracts/test/v0.7/StalenessFlaggingValidator.test.ts deleted file mode 100644 index 8a5c4b67632..00000000000 --- a/contracts/test/v0.7/StalenessFlaggingValidator.test.ts +++ /dev/null @@ -1,632 +0,0 @@ -import { ethers } from 'hardhat' -import { - evmWordToAddress, - getLog, - getLogs, - numToBytes32, - publicAbi, -} from '../test-helpers/helpers' -import { assert, expect } from 'chai' -import { BigNumber, Contract, ContractFactory } from 'ethers' -import { Personas, getUsers } from '../test-helpers/setup' -import { evmRevert } from '../test-helpers/matchers' - -let personas: Personas -let validatorFactory: ContractFactory -let flagsFactory: ContractFactory -let acFactory: ContractFactory -let aggregatorFactory: ContractFactory - -before(async () => { - personas = (await getUsers()).personas - - validatorFactory = await ethers.getContractFactory( - 'src/v0.7/dev/StalenessFlaggingValidator.sol:StalenessFlaggingValidator', - personas.Carol, - ) - flagsFactory = await ethers.getContractFactory( - 'src/v0.6/Flags.sol:Flags', - personas.Carol, - ) - acFactory = await ethers.getContractFactory( - 'src/v0.6/SimpleWriteAccessController.sol:SimpleWriteAccessController', - personas.Carol, - ) - aggregatorFactory = await ethers.getContractFactory( - 'src/v0.7/tests/MockV3Aggregator.sol:MockV3Aggregator', - personas.Carol, - ) -}) - -describe('StalenessFlaggingValidator', () => { - let validator: Contract - let flags: Contract - let ac: Contract - - const flaggingThreshold1 = 10000 - const flaggingThreshold2 = 20000 - - beforeEach(async () => { - ac = await acFactory.connect(personas.Carol).deploy() - flags = await flagsFactory.connect(personas.Carol).deploy(ac.address) - validator = await validatorFactory - .connect(personas.Carol) - .deploy(flags.address) - - await ac.connect(personas.Carol).addAccess(validator.address) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(validator, [ - 'update', - 'check', - 'setThresholds', - 'setFlagsAddress', - 'threshold', - 'flags', - // Upkeep methods: - 'checkUpkeep', - 'performUpkeep', - // Owned methods: - 'acceptOwnership', - 'owner', - 'transferOwnership', - ]) - }) - - describe('#constructor', () => { - it('sets the arguments passed in', async () => { - assert.equal(await validator.flags(), flags.address) - }) - - it('sets the owner', async () => { - assert.equal(await validator.owner(), await personas.Carol.getAddress()) - }) - }) - - describe('#setFlagsAddress', () => { - const newFlagsAddress = '0x0123456789012345678901234567890123456789' - - it('changes the flags address', async () => { - assert.equal(flags.address, await validator.flags()) - - await validator.connect(personas.Carol).setFlagsAddress(newFlagsAddress) - - assert.equal(newFlagsAddress, await validator.flags()) - }) - - it('emits a log event only when actually changed', async () => { - const tx = await validator - .connect(personas.Carol) - .setFlagsAddress(newFlagsAddress) - await expect(tx) - .to.emit(validator, 'FlagsAddressUpdated') - .withArgs(flags.address, newFlagsAddress) - - const sameChangeTx = await validator - .connect(personas.Carol) - .setFlagsAddress(newFlagsAddress) - - await expect(sameChangeTx).to.not.emit(validator, 'FlagsAddressUpdated') - }) - - describe('when called by a non-owner', () => { - it('reverts', async () => { - await evmRevert( - validator.connect(personas.Neil).setFlagsAddress(newFlagsAddress), - 'Only callable by owner', - ) - }) - }) - }) - - describe('#setThresholds', () => { - let agg1: Contract - let agg2: Contract - let aggregators: Array - let thresholds: Array - - beforeEach(async () => { - const decimals = 8 - const initialAnswer = 10000000000 - agg1 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - agg2 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - }) - - describe('failure', () => { - beforeEach(() => { - aggregators = [agg1.address, agg2.address] - thresholds = [flaggingThreshold1] - }) - - it('reverts when called by a non-owner', async () => { - await evmRevert( - validator - .connect(personas.Neil) - .setThresholds(aggregators, thresholds), - 'Only callable by owner', - ) - }) - - it('reverts when passed uneven arrays', async () => { - await evmRevert( - validator - .connect(personas.Carol) - .setThresholds(aggregators, thresholds), - 'Different sized arrays', - ) - }) - }) - - describe('success', () => { - let tx: any - - beforeEach(() => { - aggregators = [agg1.address, agg2.address] - thresholds = [flaggingThreshold1, flaggingThreshold2] - }) - - describe('when called with 2 new thresholds', () => { - beforeEach(async () => { - tx = await validator - .connect(personas.Carol) - .setThresholds(aggregators, thresholds) - }) - - it('sets the thresholds', async () => { - const first = await validator.threshold(agg1.address) - const second = await validator.threshold(agg2.address) - assert.equal(first.toString(), flaggingThreshold1.toString()) - assert.equal(second.toString(), flaggingThreshold2.toString()) - }) - - it('emits events', async () => { - const firstEvent = await getLog(tx, 0) - assert.equal(evmWordToAddress(firstEvent.topics[1]), agg1.address) - assert.equal(firstEvent.topics[3], numToBytes32(flaggingThreshold1)) - const secondEvent = await getLog(tx, 1) - assert.equal(evmWordToAddress(secondEvent.topics[1]), agg2.address) - assert.equal(secondEvent.topics[3], numToBytes32(flaggingThreshold2)) - }) - }) - - describe('when called with 2, but 1 has not changed', () => { - it('emits only 1 event', async () => { - tx = await validator - .connect(personas.Carol) - .setThresholds(aggregators, thresholds) - - const newThreshold = flaggingThreshold2 + 1 - tx = await validator - .connect(personas.Carol) - .setThresholds(aggregators, [flaggingThreshold1, newThreshold]) - const logs = await getLogs(tx) - assert.equal(logs.length, 1) - const log = logs[0] - assert.equal(evmWordToAddress(log.topics[1]), agg2.address) - assert.equal(log.topics[2], numToBytes32(flaggingThreshold2)) - assert.equal(log.topics[3], numToBytes32(newThreshold)) - }) - }) - }) - }) - - describe('#check', () => { - let agg1: Contract - let agg2: Contract - let aggregators: Array - let thresholds: Array - const decimals = 8 - const initialAnswer = 10000000000 - beforeEach(async () => { - agg1 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - agg2 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - aggregators = [agg1.address, agg2.address] - thresholds = [flaggingThreshold1, flaggingThreshold2] - await validator.setThresholds(aggregators, thresholds) - }) - - describe('when neither are stale', () => { - it('returns an empty array', async () => { - const response = await validator.check(aggregators) - assert.equal(response.length, 0) - }) - }) - - describe('when threshold is not set in the validator', () => { - it('returns an empty array', async () => { - const agg3 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - const response = await validator.check([agg3.address]) - assert.equal(response.length, 0) - }) - }) - - describe('when one of the aggregators is stale', () => { - it('returns an array with one stale aggregator', async () => { - const currentTimestamp = await agg1.latestTimestamp() - const staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - const response = await validator.check(aggregators) - - assert.equal(response.length, 1) - assert.equal(response[0], agg1.address) - }) - }) - - describe('When both aggregators are stale', () => { - it('returns an array with both aggregators', async () => { - let currentTimestamp = await agg1.latestTimestamp() - let staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - currentTimestamp = await agg2.latestTimestamp() - staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold2 + 1), - ) - await agg2.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - const response = await validator.check(aggregators) - - assert.equal(response.length, 2) - assert.equal(response[0], agg1.address) - assert.equal(response[1], agg2.address) - }) - }) - }) - - describe('#update', () => { - let agg1: Contract - let agg2: Contract - let aggregators: Array - let thresholds: Array - const decimals = 8 - const initialAnswer = 10000000000 - beforeEach(async () => { - agg1 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - agg2 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - aggregators = [agg1.address, agg2.address] - thresholds = [flaggingThreshold1, flaggingThreshold2] - await validator.setThresholds(aggregators, thresholds) - }) - - describe('when neither are stale', () => { - it('does not raise a flag', async () => { - const tx = await validator.update(aggregators) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - - describe('when threshold is not set in the validator', () => { - it('does not raise a flag', async () => { - const agg3 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - const tx = await validator.update([agg3.address]) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - - describe('when one is stale', () => { - it('raises a flag for that aggregator', async () => { - const currentTimestamp = await agg1.latestTimestamp() - const staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - const tx = await validator.update(aggregators) - const logs = await getLogs(tx) - assert.equal(logs.length, 1) - assert.equal(evmWordToAddress(logs[0].topics[1]), agg1.address) - }) - }) - - describe('when both are stale', () => { - it('raises 2 flags, one for each aggregator', async () => { - let currentTimestamp = await agg1.latestTimestamp() - let staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - currentTimestamp = await agg2.latestTimestamp() - staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold2 + 1), - ) - await agg2.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - const tx = await validator.update(aggregators) - const logs = await getLogs(tx) - assert.equal(logs.length, 2) - assert.equal(evmWordToAddress(logs[0].topics[1]), agg1.address) - assert.equal(evmWordToAddress(logs[1].topics[1]), agg2.address) - }) - }) - }) - - describe('#checkUpkeep', () => { - let agg1: Contract - let agg2: Contract - let aggregators: Array - let thresholds: Array - const decimals = 8 - const initialAnswer = 10000000000 - beforeEach(async () => { - agg1 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - agg2 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - aggregators = [agg1.address, agg2.address] - thresholds = [flaggingThreshold1, flaggingThreshold2] - await validator.setThresholds(aggregators, thresholds) - }) - - describe('when neither are stale', () => { - it('returns false and an empty array', async () => { - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const response = await validator.checkUpkeep(bytesData) - - assert.equal(response[0], false) - const decodedResponse = ethers.utils.defaultAbiCoder.decode( - ['address[]'], - response?.[1], - ) - assert.equal(decodedResponse[0].length, 0) - }) - }) - - describe('when threshold is not set in the validator', () => { - it('returns flase and an empty array', async () => { - const agg3 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [[agg3.address]], - ) - const response = await validator.checkUpkeep(bytesData) - - assert.equal(response[0], false) - const decodedResponse = ethers.utils.defaultAbiCoder.decode( - ['address[]'], - response?.[1], - ) - assert.equal(decodedResponse[0].length, 0) - }) - }) - - describe('when one of the aggregators is stale', () => { - it('returns true with an array with one stale aggregator', async () => { - const currentTimestamp = await agg1.latestTimestamp() - const staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const response = await validator.checkUpkeep(bytesData) - - assert.equal(response[0], true) - const decodedResponse = ethers.utils.defaultAbiCoder.decode( - ['address[]'], - response?.[1], - ) - const decodedArray = decodedResponse[0] - assert.equal(decodedArray.length, 1) - assert.equal(decodedArray[0], agg1.address) - }) - }) - - describe('When both aggregators are stale', () => { - it('returns true with an array with both aggregators', async () => { - let currentTimestamp = await agg1.latestTimestamp() - let staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - currentTimestamp = await agg2.latestTimestamp() - staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold2 + 1), - ) - await agg2.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const response = await validator.checkUpkeep(bytesData) - - assert.equal(response[0], true) - const decodedResponse = ethers.utils.defaultAbiCoder.decode( - ['address[]'], - response?.[1], - ) - const decodedArray = decodedResponse[0] - assert.equal(decodedArray.length, 2) - assert.equal(decodedArray[0], agg1.address) - assert.equal(decodedArray[1], agg2.address) - }) - }) - }) - - describe('#performUpkeep', () => { - let agg1: Contract - let agg2: Contract - let aggregators: Array - let thresholds: Array - const decimals = 8 - const initialAnswer = 10000000000 - beforeEach(async () => { - agg1 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - agg2 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - aggregators = [agg1.address, agg2.address] - thresholds = [flaggingThreshold1, flaggingThreshold2] - await validator.setThresholds(aggregators, thresholds) - }) - - describe('when neither are stale', () => { - it('does not raise a flag', async () => { - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const tx = await validator.performUpkeep(bytesData) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - - describe('when threshold is not set in the validator', () => { - it('does not raise a flag', async () => { - const agg3 = await aggregatorFactory - .connect(personas.Carol) - .deploy(decimals, initialAnswer) - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [[agg3.address]], - ) - const tx = await validator.performUpkeep(bytesData) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - - describe('when one is stale', () => { - it('raises a flag for that aggregator', async () => { - const currentTimestamp = await agg1.latestTimestamp() - const staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const tx = await validator.performUpkeep(bytesData) - const logs = await getLogs(tx) - assert.equal(logs.length, 1) - assert.equal(evmWordToAddress(logs[0].topics[1]), agg1.address) - }) - }) - - describe('when both are stale', () => { - it('raises 2 flags, one for each aggregator', async () => { - let currentTimestamp = await agg1.latestTimestamp() - let staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold1 + 1), - ) - await agg1.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - currentTimestamp = await agg2.latestTimestamp() - staleTimestamp = currentTimestamp.sub( - BigNumber.from(flaggingThreshold2 + 1), - ) - await agg2.updateRoundData( - 99, - initialAnswer, - staleTimestamp, - staleTimestamp, - ) - - const bytesData = ethers.utils.defaultAbiCoder.encode( - ['address[]'], - [aggregators], - ) - const tx = await validator.performUpkeep(bytesData) - const logs = await getLogs(tx) - assert.equal(logs.length, 2) - assert.equal(evmWordToAddress(logs[0].topics[1]), agg1.address) - assert.equal(evmWordToAddress(logs[1].topics[1]), agg2.address) - }) - }) - }) -}) diff --git a/contracts/test/v0.7/UpkeepRegistrationRequests.test.ts b/contracts/test/v0.7/UpkeepRegistrationRequests.test.ts deleted file mode 100644 index 5ec9306c668..00000000000 --- a/contracts/test/v0.7/UpkeepRegistrationRequests.test.ts +++ /dev/null @@ -1,603 +0,0 @@ -import { ethers } from 'hardhat' -import { assert, expect } from 'chai' -import { evmRevert } from '../test-helpers/matchers' -import { getUsers, Personas } from '../test-helpers/setup' -import { BigNumber, Signer } from 'ethers' -import { LinkToken__factory as LinkTokenFactory } from '../../typechain/factories/LinkToken__factory' -import { KeeperRegistry1_1__factory as KeeperRegistryFactory } from '../../typechain/factories/KeeperRegistry1_1__factory' -import { MockV3Aggregator__factory as MockV3AggregatorFactory } from '../../typechain/factories/MockV3Aggregator__factory' -import { UpkeepRegistrationRequests__factory as UpkeepRegistrationRequestsFactory } from '../../typechain/factories/UpkeepRegistrationRequests__factory' -import { UpkeepMock__factory as UpkeepMockFactory } from '../../typechain/factories/UpkeepMock__factory' -import { KeeperRegistry1_1 as KeeperRegistry } from '../../typechain/KeeperRegistry1_1' -import { UpkeepRegistrationRequests } from '../../typechain/UpkeepRegistrationRequests' -import { MockV3Aggregator } from '../../typechain/MockV3Aggregator' -import { LinkToken } from '../../typechain/LinkToken' -import { UpkeepMock } from '../../typechain/UpkeepMock' - -let linkTokenFactory: LinkTokenFactory -let mockV3AggregatorFactory: MockV3AggregatorFactory -let keeperRegistryFactory: KeeperRegistryFactory -let upkeepRegistrationRequestsFactory: UpkeepRegistrationRequestsFactory -let upkeepMockFactory: UpkeepMockFactory - -let personas: Personas - -before(async () => { - personas = (await getUsers()).personas - - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - ) - mockV3AggregatorFactory = (await ethers.getContractFactory( - 'src/v0.7/tests/MockV3Aggregator.sol:MockV3Aggregator', - )) as unknown as MockV3AggregatorFactory - // @ts-ignore bug in autogen file - keeperRegistryFactory = await ethers.getContractFactory('KeeperRegistry1_1') - upkeepRegistrationRequestsFactory = await ethers.getContractFactory( - 'UpkeepRegistrationRequests', - ) - upkeepMockFactory = await ethers.getContractFactory('UpkeepMock') -}) - -const errorMsgs = { - onlyOwner: 'revert Only callable by owner', - onlyAdmin: 'only admin / owner can cancel', - hashPayload: 'hash and payload do not match', - requestNotFound: 'request not found', -} - -describe('UpkeepRegistrationRequests', () => { - const upkeepName = 'SampleUpkeep' - - const linkEth = BigNumber.from(300000000) - const gasWei = BigNumber.from(100) - const executeGas = BigNumber.from(100000) - const source = BigNumber.from(100) - const paymentPremiumPPB = BigNumber.from(250000000) - const flatFeeMicroLink = BigNumber.from(0) - - const window_big = BigNumber.from(1000) - const window_small = BigNumber.from(2) - const threshold_big = BigNumber.from(1000) - const threshold_small = BigNumber.from(5) - - const blockCountPerTurn = BigNumber.from(3) - const emptyBytes = '0x00' - const stalenessSeconds = BigNumber.from(43820) - const gasCeilingMultiplier = BigNumber.from(1) - const maxCheckGas = BigNumber.from(20000000) - const fallbackGasPrice = BigNumber.from(200) - const fallbackLinkPrice = BigNumber.from(200000000) - const minLINKJuels = BigNumber.from('1000000000000000000') - const amount = BigNumber.from('5000000000000000000') - const amount1 = BigNumber.from('6000000000000000000') - - let owner: Signer - let admin: Signer - let someAddress: Signer - let registrarOwner: Signer - let stranger: Signer - - let linkToken: LinkToken - let linkEthFeed: MockV3Aggregator - let gasPriceFeed: MockV3Aggregator - let registry: KeeperRegistry - let mock: UpkeepMock - let registrar: UpkeepRegistrationRequests - - beforeEach(async () => { - owner = personas.Default - admin = personas.Neil - someAddress = personas.Ned - registrarOwner = personas.Nelly - stranger = personas.Nancy - - linkToken = await linkTokenFactory.connect(owner).deploy() - gasPriceFeed = await mockV3AggregatorFactory - .connect(owner) - .deploy(0, gasWei) - linkEthFeed = await mockV3AggregatorFactory - .connect(owner) - .deploy(9, linkEth) - registry = await keeperRegistryFactory - .connect(owner) - .deploy( - linkToken.address, - linkEthFeed.address, - gasPriceFeed.address, - paymentPremiumPPB, - flatFeeMicroLink, - blockCountPerTurn, - maxCheckGas, - stalenessSeconds, - gasCeilingMultiplier, - fallbackGasPrice, - fallbackLinkPrice, - ) - - mock = await upkeepMockFactory.deploy() - - registrar = await upkeepRegistrationRequestsFactory - .connect(registrarOwner) - .deploy(linkToken.address, minLINKJuels) - - await registry.setRegistrar(registrar.address) - }) - - describe('#typeAndVersion', () => { - it('uses the correct type and version', async () => { - const typeAndVersion = await registrar.typeAndVersion() - assert.equal(typeAndVersion, 'UpkeepRegistrationRequests 1.0.0') - }) - }) - - describe('#register', () => { - it('reverts if not called by the LINK token', async () => { - await evmRevert( - registrar - .connect(someAddress) - .register( - upkeepName, - emptyBytes, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - amount, - source, - ), - 'Must use LINK token', - ) - }) - - it('reverts if the amount passed in data mismatches actual amount sent', async () => { - await registrar - .connect(registrarOwner) - .setRegistrationConfig( - true, - window_small, - threshold_big, - registry.address, - minLINKJuels, - ) - - const abiEncodedBytes = registrar.interface.encodeFunctionData( - 'register', - [ - upkeepName, - emptyBytes, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - amount1, - source, - ], - ) - - await evmRevert( - linkToken.transferAndCall(registrar.address, amount, abiEncodedBytes), - 'Amount mismatch', - ) - }) - - it('reverts if the admin address is 0x0000...', async () => { - const abiEncodedBytes = registrar.interface.encodeFunctionData( - 'register', - [ - upkeepName, - emptyBytes, - mock.address, - executeGas, - '0x0000000000000000000000000000000000000000', - emptyBytes, - amount, - source, - ], - ) - - await evmRevert( - linkToken.transferAndCall(registrar.address, amount, abiEncodedBytes), - 'Unable to create request', - ) - }) - - it('Auto Approve ON - registers an upkeep on KeeperRegistry instantly and emits both RegistrationRequested and RegistrationApproved events', async () => { - //get current upkeep count - const upkeepCount = await registry.getUpkeepCount() - - //set auto approve ON with high threshold limits - await registrar - .connect(registrarOwner) - .setRegistrationConfig( - true, - window_small, - threshold_big, - registry.address, - minLINKJuels, - ) - - //register with auto approve ON - const abiEncodedBytes = registrar.interface.encodeFunctionData( - 'register', - [ - upkeepName, - emptyBytes, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - amount, - source, - ], - ) - const tx = await linkToken.transferAndCall( - registrar.address, - amount, - abiEncodedBytes, - ) - - //confirm if a new upkeep has been registered and the details are the same as the one just registered - const newupkeep = await registry.getUpkeep(upkeepCount) - assert.equal(newupkeep.target, mock.address) - assert.equal(newupkeep.admin, await admin.getAddress()) - assert.equal(newupkeep.checkData, emptyBytes) - assert.equal(newupkeep.balance.toString(), amount.toString()) - assert.equal(newupkeep.executeGas, executeGas.toNumber()) - - await expect(tx).to.emit(registrar, 'RegistrationRequested') - await expect(tx).to.emit(registrar, 'RegistrationApproved') - }) - - it('Auto Approve OFF - does not registers an upkeep on KeeperRegistry, emits only RegistrationRequested event', async () => { - //get upkeep count before attempting registration - const beforeCount = await registry.getUpkeepCount() - - //set auto approve OFF, threshold limits dont matter in this case - await registrar - .connect(registrarOwner) - .setRegistrationConfig( - false, - window_small, - threshold_big, - registry.address, - minLINKJuels, - ) - - //register with auto approve OFF - const abiEncodedBytes = registrar.interface.encodeFunctionData( - 'register', - [ - upkeepName, - emptyBytes, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - amount, - source, - ], - ) - const tx = await linkToken.transferAndCall( - registrar.address, - amount, - abiEncodedBytes, - ) - const receipt = await tx.wait() - - //get upkeep count after attempting registration - const afterCount = await registry.getUpkeepCount() - //confirm that a new upkeep has NOT been registered and upkeep count is still the same - assert.deepEqual(beforeCount, afterCount) - - //confirm that only RegistrationRequested event is emitted and RegistrationApproved event is not - await expect(tx).to.emit(registrar, 'RegistrationRequested') - await expect(tx).not.to.emit(registrar, 'RegistrationApproved') - - const hash = receipt.logs[2].topics[1] - const pendingRequest = await registrar.getPendingRequest(hash) - assert.equal(await admin.getAddress(), pendingRequest[0]) - assert.ok(amount.eq(pendingRequest[1])) - }) - - it('Auto Approve ON - Throttle max approvals - does not registers an upkeep on KeeperRegistry beyond the throttle limit, emits only RegistrationRequested event after throttle starts', async () => { - //get upkeep count before attempting registration - const beforeCount = await registry.getUpkeepCount() - - //set auto approve on, with low threshold limits - await registrar - .connect(registrarOwner) - .setRegistrationConfig( - true, - window_big, - threshold_small, - registry.address, - minLINKJuels, - ) - - let abiEncodedBytes = registrar.interface.encodeFunctionData('register', [ - upkeepName, - emptyBytes, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - amount, - source, - ]) - - //register within threshold, new upkeep should be registered - await linkToken.transferAndCall( - registrar.address, - amount, - abiEncodedBytes, - ) - const intermediateCount = await registry.getUpkeepCount() - //make sure 1 upkeep was registered - assert.equal(beforeCount.toNumber() + 1, intermediateCount.toNumber()) - - //try registering more than threshold(say 2x), new upkeeps should not be registered after the threshold amount is reached - for (let step = 0; step < threshold_small.toNumber() * 2; step++) { - abiEncodedBytes = registrar.interface.encodeFunctionData('register', [ - upkeepName, - emptyBytes, - mock.address, - executeGas.toNumber() + step, // make unique hash - await admin.getAddress(), - emptyBytes, - amount, - source, - ]) - - await linkToken.transferAndCall( - registrar.address, - amount, - abiEncodedBytes, - ) - } - const afterCount = await registry.getUpkeepCount() - //count of newly registered upkeeps should be equal to the threshold set for auto approval - const newRegistrationsCount = - afterCount.toNumber() - beforeCount.toNumber() - assert( - newRegistrationsCount == threshold_small.toNumber(), - 'Registrations beyond threshold', - ) - }) - }) - - describe('#approve', () => { - let hash: string - - beforeEach(async () => { - await registrar - .connect(registrarOwner) - .setRegistrationConfig( - false, - window_small, - threshold_big, - registry.address, - minLINKJuels, - ) - - //register with auto approve OFF - const abiEncodedBytes = registrar.interface.encodeFunctionData( - 'register', - [ - upkeepName, - emptyBytes, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - amount, - source, - ], - ) - - const tx = await linkToken.transferAndCall( - registrar.address, - amount, - abiEncodedBytes, - ) - const receipt = await tx.wait() - hash = receipt.logs[2].topics[1] - }) - - it('reverts if not called by the owner', async () => { - const tx = registrar - .connect(stranger) - .approve( - upkeepName, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - hash, - ) - await evmRevert(tx, 'Only callable by owner') - }) - - it('reverts if the hash does not exist', async () => { - const tx = registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - '0x000000000000000000000000322813fd9a801c5507c9de605d63cea4f2ce6c44', - ) - await evmRevert(tx, errorMsgs.requestNotFound) - }) - - it('reverts if any member of the payload changes', async () => { - let tx = registrar - .connect(registrarOwner) - .approve( - upkeepName, - ethers.Wallet.createRandom().address, - executeGas, - await admin.getAddress(), - emptyBytes, - hash, - ) - await evmRevert(tx, errorMsgs.hashPayload) - tx = registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - 10000, - await admin.getAddress(), - emptyBytes, - hash, - ) - await evmRevert(tx, errorMsgs.hashPayload) - tx = registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - executeGas, - ethers.Wallet.createRandom().address, - emptyBytes, - hash, - ) - await evmRevert(tx, errorMsgs.hashPayload) - tx = registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - executeGas, - await admin.getAddress(), - '0x1234', - hash, - ) - await evmRevert(tx, errorMsgs.hashPayload) - }) - - it('approves an existing registration request', async () => { - const tx = await registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - hash, - ) - await expect(tx).to.emit(registrar, 'RegistrationApproved') - }) - - it('deletes the request afterwards / reverts if the request DNE', async () => { - await registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - hash, - ) - const tx = registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - hash, - ) - await evmRevert(tx, errorMsgs.requestNotFound) - }) - }) - - describe('#cancel', () => { - let hash: string - - beforeEach(async () => { - await registrar - .connect(registrarOwner) - .setRegistrationConfig( - false, - window_small, - threshold_big, - registry.address, - minLINKJuels, - ) - - //register with auto approve OFF - const abiEncodedBytes = registrar.interface.encodeFunctionData( - 'register', - [ - upkeepName, - emptyBytes, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - amount, - source, - ], - ) - const tx = await linkToken.transferAndCall( - registrar.address, - amount, - abiEncodedBytes, - ) - const receipt = await tx.wait() - hash = receipt.logs[2].topics[1] - // submit duplicate request (increase balance) - await linkToken.transferAndCall( - registrar.address, - amount, - abiEncodedBytes, - ) - }) - - it('reverts if not called by the admin / owner', async () => { - const tx = registrar.connect(stranger).cancel(hash) - await evmRevert(tx, errorMsgs.onlyAdmin) - }) - - it('reverts if the hash does not exist', async () => { - const tx = registrar - .connect(registrarOwner) - .cancel( - '0x000000000000000000000000322813fd9a801c5507c9de605d63cea4f2ce6c44', - ) - await evmRevert(tx, 'request not found') - }) - - it('refunds the total request balance to the admin address', async () => { - const before = await linkToken.balanceOf(await admin.getAddress()) - const tx = await registrar.connect(admin).cancel(hash) - const after = await linkToken.balanceOf(await admin.getAddress()) - assert.isTrue(after.sub(before).eq(amount.mul(BigNumber.from(2)))) - await expect(tx).to.emit(registrar, 'RegistrationRejected') - }) - - it('deletes the request hash', async () => { - await registrar.connect(registrarOwner).cancel(hash) - let tx = registrar.connect(registrarOwner).cancel(hash) - await evmRevert(tx, errorMsgs.requestNotFound) - tx = registrar - .connect(registrarOwner) - .approve( - upkeepName, - mock.address, - executeGas, - await admin.getAddress(), - emptyBytes, - hash, - ) - await evmRevert(tx, errorMsgs.requestNotFound) - }) - }) -}) diff --git a/contracts/test/v0.7/VRFD20.test.ts b/contracts/test/v0.7/VRFD20.test.ts deleted file mode 100644 index f1e0e9ab0a8..00000000000 --- a/contracts/test/v0.7/VRFD20.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { ethers } from 'hardhat' -import { assert, expect } from 'chai' -import { - BigNumber, - constants, - Contract, - ContractFactory, - ContractTransaction, -} from 'ethers' -import { getUsers, Personas, Roles } from '../test-helpers/setup' -import { - evmWordToAddress, - getLog, - publicAbi, - toBytes32String, - toWei, - numToBytes32, - getLogs, -} from '../test-helpers/helpers' - -let roles: Roles -let personas: Personas -let linkTokenFactory: ContractFactory -let vrfCoordinatorMockFactory: ContractFactory -let vrfD20Factory: ContractFactory - -before(async () => { - const users = await getUsers() - - roles = users.roles - personas = users.personas - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) - vrfCoordinatorMockFactory = await ethers.getContractFactory( - 'src/v0.7/tests/VRFCoordinatorMock.sol:VRFCoordinatorMock', - roles.defaultAccount, - ) - vrfD20Factory = await ethers.getContractFactory( - 'src/v0.6/examples/VRFD20.sol:VRFD20', - roles.defaultAccount, - ) -}) - -describe('VRFD20', () => { - const deposit = toWei('1') - const fee = toWei('0.1') - const keyHash = toBytes32String('keyHash') - - let link: Contract - let vrfCoordinator: Contract - let vrfD20: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - vrfCoordinator = await vrfCoordinatorMockFactory - .connect(roles.defaultAccount) - .deploy(link.address) - vrfD20 = await vrfD20Factory - .connect(roles.defaultAccount) - .deploy(vrfCoordinator.address, link.address, keyHash, fee) - await link.transfer(vrfD20.address, deposit) - }) - - it('has a limited public interface [ @skip-coverage ]', () => { - publicAbi(vrfD20, [ - // Owned - 'acceptOwnership', - 'owner', - 'transferOwnership', - //VRFConsumerBase - 'rawFulfillRandomness', - // VRFD20 - 'rollDice', - 'house', - 'withdrawLINK', - 'keyHash', - 'fee', - 'setKeyHash', - 'setFee', - ]) - }) - - describe('#withdrawLINK', () => { - describe('failure', () => { - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20 - .connect(roles.stranger) - .withdrawLINK(await roles.stranger.getAddress(), deposit), - ).to.be.revertedWith('Only callable by owner') - }) - - it('reverts when not enough LINK in the contract', async () => { - const withdrawAmount = deposit.mul(2) - await expect( - vrfD20 - .connect(roles.defaultAccount) - .withdrawLINK( - await roles.defaultAccount.getAddress(), - withdrawAmount, - ), - ).to.be.reverted - }) - }) - - describe('success', () => { - it('withdraws LINK', async () => { - const startingAmount = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - const expectedAmount = BigNumber.from(startingAmount).add(deposit) - await vrfD20 - .connect(roles.defaultAccount) - .withdrawLINK(await roles.defaultAccount.getAddress(), deposit) - const actualAmount = await link.balanceOf( - await roles.defaultAccount.getAddress(), - ) - assert.equal(actualAmount.toString(), expectedAmount.toString()) - }) - }) - }) - - describe('#setKeyHash', () => { - const newHash = toBytes32String('newhash') - - describe('failure', () => { - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20.connect(roles.stranger).setKeyHash(newHash), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('success', () => { - it('sets the key hash', async () => { - await vrfD20.setKeyHash(newHash) - const actualHash = await vrfD20.keyHash() - assert.equal(actualHash, newHash) - }) - }) - }) - - describe('#setFee', () => { - const newFee = 1234 - - describe('failure', () => { - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20.connect(roles.stranger).setFee(newFee), - ).to.be.revertedWith('Only callable by owner') - }) - }) - - describe('success', () => { - it('sets the fee', async () => { - await vrfD20.setFee(newFee) - const actualFee = await vrfD20.fee() - assert.equal(actualFee.toString(), newFee.toString()) - }) - }) - }) - - describe('#house', () => { - describe('failure', () => { - it('reverts when dice not rolled', async () => { - await expect( - vrfD20.house(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Dice not rolled') - }) - - it('reverts when dice roll is in progress', async () => { - await vrfD20.rollDice(await personas.Nancy.getAddress()) - await expect( - vrfD20.house(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Roll in progress') - }) - }) - - describe('success', () => { - it('returns the correct house', async () => { - const randomness = 98765 - const expectedHouse = 'Martell' - const tx = await vrfD20.rollDice(await personas.Nancy.getAddress()) - const log = await getLog(tx, 3) - const eventRequestId = log?.topics?.[1] - await vrfCoordinator.callBackWithRandomness( - eventRequestId, - randomness, - vrfD20.address, - ) - const response = await vrfD20.house(await personas.Nancy.getAddress()) - assert.equal(response.toString(), expectedHouse) - }) - }) - }) - - describe('#rollDice', () => { - describe('success', () => { - let tx: ContractTransaction - beforeEach(async () => { - tx = await vrfD20.rollDice(await personas.Nancy.getAddress()) - }) - - it('emits a RandomnessRequest event from the VRFCoordinator', async () => { - const log = await getLog(tx, 2) - const topics = log?.topics - assert.equal(evmWordToAddress(topics?.[1]), vrfD20.address) - assert.equal(topics?.[2], keyHash) - assert.equal(topics?.[3], constants.HashZero) - }) - }) - - describe('failure', () => { - it('reverts when LINK balance is zero', async () => { - const vrfD202 = await vrfD20Factory - .connect(roles.defaultAccount) - .deploy(vrfCoordinator.address, link.address, keyHash, fee) - await expect( - vrfD202.rollDice(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Not enough LINK to pay fee') - }) - - it('reverts when called by a non-owner', async () => { - await expect( - vrfD20 - .connect(roles.stranger) - .rollDice(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Only callable by owner') - }) - - it('reverts when the roller rolls more than once', async () => { - await vrfD20.rollDice(await personas.Nancy.getAddress()) - await expect( - vrfD20.rollDice(await personas.Nancy.getAddress()), - ).to.be.revertedWith('Already rolled') - }) - }) - }) - - describe('#fulfillRandomness', () => { - const randomness = 98765 - const expectedModResult = (randomness % 20) + 1 - const expectedHouse = 'Martell' - let eventRequestId: string - beforeEach(async () => { - const tx = await vrfD20.rollDice(await personas.Nancy.getAddress()) - const log = await getLog(tx, 3) - eventRequestId = log?.topics?.[1] - }) - - describe('success', () => { - let tx: ContractTransaction - beforeEach(async () => { - tx = await vrfCoordinator.callBackWithRandomness( - eventRequestId, - randomness, - vrfD20.address, - ) - }) - - it('emits a DiceLanded event', async () => { - const log = await getLog(tx, 0) - assert.equal(log?.topics[1], eventRequestId) - assert.equal(log?.topics[2], numToBytes32(expectedModResult)) - }) - - it('sets the correct dice roll result', async () => { - const response = await vrfD20.house(await personas.Nancy.getAddress()) - assert.equal(response.toString(), expectedHouse) - }) - - it('allows someone else to roll', async () => { - const secondRandomness = 55555 - tx = await vrfD20.rollDice(await personas.Ned.getAddress()) - const log = await getLog(tx, 3) - eventRequestId = log?.topics?.[1] - tx = await vrfCoordinator.callBackWithRandomness( - eventRequestId, - secondRandomness, - vrfD20.address, - ) - }) - }) - - describe('failure', () => { - it('does not fulfill when fulfilled by the wrong VRFcoordinator', async () => { - const vrfCoordinator2 = await vrfCoordinatorMockFactory - .connect(roles.defaultAccount) - .deploy(link.address) - - const tx = await vrfCoordinator2.callBackWithRandomness( - eventRequestId, - randomness, - vrfD20.address, - ) - const logs = await getLogs(tx) - assert.equal(logs.length, 0) - }) - }) - }) -}) diff --git a/contracts/test/v0.7/gasUsage.test.ts b/contracts/test/v0.7/gasUsage.test.ts deleted file mode 100644 index 97146622d06..00000000000 --- a/contracts/test/v0.7/gasUsage.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ethers } from 'hardhat' -import { toBytes32String, toWei } from '../test-helpers/helpers' -import { Contract, ContractFactory } from 'ethers' -import { getUsers, Roles } from '../test-helpers/setup' -import { - convertFufillParams, - convertFulfill2Params, - decodeRunRequest, -} from '../test-helpers/oracle' -import { gasDiffLessThan } from '../test-helpers/matchers' - -let operatorFactory: ContractFactory -let oracleFactory: ContractFactory -let basicConsumerFactory: ContractFactory -let linkTokenFactory: ContractFactory - -let roles: Roles - -before(async () => { - const users = await getUsers() - - roles = users.roles - operatorFactory = await ethers.getContractFactory( - 'src/v0.7/Operator.sol:Operator', - roles.defaultAccount, - ) - oracleFactory = await ethers.getContractFactory( - 'src/v0.6/Oracle.sol:Oracle', - roles.defaultAccount, - ) - basicConsumerFactory = await ethers.getContractFactory( - 'src/v0.6/tests/BasicConsumer.sol:BasicConsumer', - roles.defaultAccount, - ) - linkTokenFactory = await ethers.getContractFactory( - 'src/v0.4/LinkToken.sol:LinkToken', - roles.defaultAccount, - ) -}) - -describe('Operator Gas Tests [ @skip-coverage ]', () => { - const specId = - '0x4c7b7ffb66b344fbaa64995af81e355a00000000000000000000000000000000' - let link: Contract - let oracle1: Contract - let operator1: Contract - let operator2: Contract - - beforeEach(async () => { - link = await linkTokenFactory.connect(roles.defaultAccount).deploy() - - operator1 = await operatorFactory - .connect(roles.defaultAccount) - .deploy(link.address, await roles.defaultAccount.getAddress()) - await operator1.setAuthorizedSenders([await roles.oracleNode.getAddress()]) - - operator2 = await operatorFactory - .connect(roles.defaultAccount) - .deploy(link.address, await roles.defaultAccount.getAddress()) - await operator2.setAuthorizedSenders([await roles.oracleNode.getAddress()]) - - oracle1 = await oracleFactory - .connect(roles.defaultAccount) - .deploy(link.address) - await oracle1.setFulfillmentPermission( - await roles.oracleNode.getAddress(), - true, - ) - }) - - // Test Oracle.fulfillOracleRequest vs Operator.fulfillOracleRequest - describe('v0.6/Oracle vs v0.7/Operator #fulfillOracleRequest', () => { - const response = 'Hi Mom!' - let basicConsumer1: Contract - let basicConsumer2: Contract - - let request1: ReturnType - let request2: ReturnType - - beforeEach(async () => { - basicConsumer1 = await basicConsumerFactory - .connect(roles.consumer) - .deploy(link.address, oracle1.address, specId) - basicConsumer2 = await basicConsumerFactory - .connect(roles.consumer) - .deploy(link.address, operator1.address, specId) - - const paymentAmount = toWei('1') - const currency = 'USD' - - await link.transfer(basicConsumer1.address, paymentAmount) - const tx1 = await basicConsumer1.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt1 = await tx1.wait() - request1 = decodeRunRequest(receipt1.logs?.[3]) - - await link.transfer(basicConsumer2.address, paymentAmount) - const tx2 = await basicConsumer2.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt2 = await tx2.wait() - request2 = decodeRunRequest(receipt2.logs?.[3]) - }) - - it('uses acceptable gas', async () => { - const tx1 = await oracle1 - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request1, response)) - const tx2 = await operator1 - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request2, response)) - const receipt1 = await tx1.wait() - const receipt2 = await tx2.wait() - // 38014 vs 40260 - gasDiffLessThan(3900, receipt1, receipt2) - }) - }) - - // Test Operator1.fulfillOracleRequest vs Operator2.fulfillOracleRequest2 - // with single word response - describe('Operator #fulfillOracleRequest vs #fulfillOracleRequest2', () => { - const response = 'Hi Mom!' - let basicConsumer1: Contract - let basicConsumer2: Contract - - let request1: ReturnType - let request2: ReturnType - - beforeEach(async () => { - basicConsumer1 = await basicConsumerFactory - .connect(roles.consumer) - .deploy(link.address, operator1.address, specId) - basicConsumer2 = await basicConsumerFactory - .connect(roles.consumer) - .deploy(link.address, operator2.address, specId) - - const paymentAmount = toWei('1') - const currency = 'USD' - - await link.transfer(basicConsumer1.address, paymentAmount) - const tx1 = await basicConsumer1.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt1 = await tx1.wait() - request1 = decodeRunRequest(receipt1.logs?.[3]) - - await link.transfer(basicConsumer2.address, paymentAmount) - const tx2 = await basicConsumer2.requestEthereumPrice( - currency, - paymentAmount, - ) - const receipt2 = await tx2.wait() - request2 = decodeRunRequest(receipt2.logs?.[3]) - }) - - it('uses acceptable gas', async () => { - const tx1 = await operator1 - .connect(roles.oracleNode) - .fulfillOracleRequest(...convertFufillParams(request1, response)) - - const responseTypes = ['bytes32'] - const responseValues = [toBytes32String(response)] - const tx2 = await operator2 - .connect(roles.oracleNode) - .fulfillOracleRequest2( - ...convertFulfill2Params(request2, responseTypes, responseValues), - ) - - const receipt1 = await tx1.wait() - const receipt2 = await tx2.wait() - gasDiffLessThan(1240, receipt1, receipt2) - }) - }) -})