diff --git a/.changeset/rude-beds-change.md b/.changeset/rude-beds-change.md new file mode 100644 index 0000000000..baf3e04216 --- /dev/null +++ b/.changeset/rude-beds-change.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +more auto 2.3 tests diff --git a/contracts/.changeset/quiet-cars-taste.md b/contracts/.changeset/quiet-cars-taste.md new file mode 100644 index 0000000000..f59f3e6d05 --- /dev/null +++ b/contracts/.changeset/quiet-cars-taste.md @@ -0,0 +1,5 @@ +--- +"@chainlink/contracts": patch +--- + +more auto 2.3 tests diff --git a/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol b/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol index 8d0fdbe522..1f8fa42f36 100644 --- a/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol +++ b/contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol @@ -251,7 +251,7 @@ contract Withdraw is SetUp { registry.withdrawERC20Fees(address(usdToken), FINANCE_ADMIN, 1); } - function test_WithdrawERC20Fees_RevertsWhenAttemptingToWithdrawLINK() public { + function test_WithdrawERC20Fees_RevertsWhen_AttemptingToWithdrawLINK() public { _mintLink(address(registry), 1e10); vm.startPrank(FINANCE_ADMIN); vm.expectRevert(Registry.InvalidToken.selector); @@ -259,6 +259,17 @@ contract Withdraw is SetUp { registry.withdrawLink(FINANCE_ADMIN, 1); // but using link withdraw functions succeeds } + function test_WithdrawERC20Fees_RevertsWhen_LinkAvailableForPaymentIsNegative() public { + _transmit(usdUpkeepID, registry); // adds USD token to finance withdrawable, and gives NOPs a LINK balance + require(registry.linkAvailableForPayment() < 0, "linkAvailableForPayment should be negative"); + vm.expectRevert(Registry.InsufficientLinkLiquidity.selector); + vm.prank(FINANCE_ADMIN); + registry.withdrawERC20Fees(address(usdToken), FINANCE_ADMIN, 1); // should revert + _mintLink(address(registry), uint256(registry.linkAvailableForPayment() * -10)); // top up LINK liquidity pool + vm.prank(FINANCE_ADMIN); + registry.withdrawERC20Fees(address(usdToken), FINANCE_ADMIN, 1); // now finance can withdraw + } + function testWithdrawERC20FeeSuccess() public { // deposit excess USDToken to the registry (this goes to the "finance withdrawable" pool be default) uint256 startReserveAmount = registry.getReserveAmount(address(usdToken)); @@ -1356,3 +1367,158 @@ contract Transmit is SetUp { ); } } + +contract MigrateReceive is SetUp { + event UpkeepMigrated(uint256 indexed id, uint256 remainingBalance, address destination); + event UpkeepReceived(uint256 indexed id, uint256 startingBalance, address importedFrom); + + Registry newRegistry; + uint256[] idsToMigrate; + + function setUp() public override { + super.setUp(); + (newRegistry, ) = deployAndConfigureRegistryAndRegistrar(AutoBase.PayoutMode.ON_CHAIN); + idsToMigrate.push(linkUpkeepID); + idsToMigrate.push(usdUpkeepID); + idsToMigrate.push(nativeUpkeepID); + registry.setPeerRegistryMigrationPermission(address(newRegistry), 1); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 2); + } + + function test_RevertsWhen_PermissionsNotSet() external { + // no permissions + registry.setPeerRegistryMigrationPermission(address(newRegistry), 0); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 0); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // only outgoing permissions + registry.setPeerRegistryMigrationPermission(address(newRegistry), 1); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 0); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // only incoming permissions + registry.setPeerRegistryMigrationPermission(address(newRegistry), 0); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 2); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // permissions opposite direction + registry.setPeerRegistryMigrationPermission(address(newRegistry), 2); + newRegistry.setPeerRegistryMigrationPermission(address(registry), 1); + vm.expectRevert(Registry.MigrationNotPermitted.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + } + + function test_RevertsWhen_ReceivingRegistryDoesNotSupportToken() external { + _removeBillingTokenConfig(newRegistry, address(weth)); + vm.expectRevert(Registry.InvalidToken.selector); + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + idsToMigrate.pop(); // remove native upkeep id + vm.prank(UPKEEP_ADMIN); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); // should succeed now + } + + function test_RevertsWhen_CalledByNonAdmin() external { + vm.expectRevert(Registry.OnlyCallableByAdmin.selector); + vm.prank(STRANGER); + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + } + + function test_Success() external { + vm.startPrank(UPKEEP_ADMIN); + + // add some changes in upkeep data to the mix + registry.pauseUpkeep(usdUpkeepID); + registry.setUpkeepTriggerConfig(linkUpkeepID, randomBytes(100)); + registry.setUpkeepCheckData(nativeUpkeepID, randomBytes(25)); + + // record previous state + uint256[] memory prevUpkeepBalances = new uint256[](3); + prevUpkeepBalances[0] = registry.getBalance(linkUpkeepID); + prevUpkeepBalances[1] = registry.getBalance(usdUpkeepID); + prevUpkeepBalances[2] = registry.getBalance(nativeUpkeepID); + uint256[] memory prevReserveBalances = new uint256[](3); + prevReserveBalances[0] = registry.getReserveAmount(address(linkToken)); + prevReserveBalances[1] = registry.getReserveAmount(address(usdToken)); + prevReserveBalances[2] = registry.getReserveAmount(address(weth)); + uint256[] memory prevTokenBalances = new uint256[](3); + prevTokenBalances[0] = linkToken.balanceOf(address(registry)); + prevTokenBalances[1] = usdToken.balanceOf(address(registry)); + prevTokenBalances[2] = weth.balanceOf(address(registry)); + bytes[] memory prevUpkeepData = new bytes[](3); + prevUpkeepData[0] = abi.encode(registry.getUpkeep(linkUpkeepID)); + prevUpkeepData[1] = abi.encode(registry.getUpkeep(usdUpkeepID)); + prevUpkeepData[2] = abi.encode(registry.getUpkeep(nativeUpkeepID)); + bytes[] memory prevUpkeepTriggerData = new bytes[](3); + prevUpkeepTriggerData[0] = registry.getUpkeepTriggerConfig(linkUpkeepID); + prevUpkeepTriggerData[1] = registry.getUpkeepTriggerConfig(usdUpkeepID); + prevUpkeepTriggerData[2] = registry.getUpkeepTriggerConfig(nativeUpkeepID); + + // event expectations + vm.expectEmit(address(registry)); + emit UpkeepMigrated(linkUpkeepID, prevUpkeepBalances[0], address(newRegistry)); + vm.expectEmit(address(registry)); + emit UpkeepMigrated(usdUpkeepID, prevUpkeepBalances[1], address(newRegistry)); + vm.expectEmit(address(registry)); + emit UpkeepMigrated(nativeUpkeepID, prevUpkeepBalances[2], address(newRegistry)); + vm.expectEmit(address(newRegistry)); + emit UpkeepReceived(linkUpkeepID, prevUpkeepBalances[0], address(registry)); + vm.expectEmit(address(newRegistry)); + emit UpkeepReceived(usdUpkeepID, prevUpkeepBalances[1], address(registry)); + vm.expectEmit(address(newRegistry)); + emit UpkeepReceived(nativeUpkeepID, prevUpkeepBalances[2], address(registry)); + + // do the thing + registry.migrateUpkeeps(idsToMigrate, address(newRegistry)); + + // assert upkeep balances have been migrated + assertEq(registry.getBalance(linkUpkeepID), 0); + assertEq(registry.getBalance(usdUpkeepID), 0); + assertEq(registry.getBalance(nativeUpkeepID), 0); + assertEq(newRegistry.getBalance(linkUpkeepID), prevUpkeepBalances[0]); + assertEq(newRegistry.getBalance(usdUpkeepID), prevUpkeepBalances[1]); + assertEq(newRegistry.getBalance(nativeUpkeepID), prevUpkeepBalances[2]); + + // assert reserve balances have been adjusted + assertEq(newRegistry.getReserveAmount(address(linkToken)), newRegistry.getBalance(linkUpkeepID)); + assertEq(newRegistry.getReserveAmount(address(usdToken)), newRegistry.getBalance(usdUpkeepID)); + assertEq(newRegistry.getReserveAmount(address(weth)), newRegistry.getBalance(nativeUpkeepID)); + assertEq( + newRegistry.getReserveAmount(address(linkToken)), + prevReserveBalances[0] - registry.getReserveAmount(address(linkToken)) + ); + assertEq( + newRegistry.getReserveAmount(address(usdToken)), + prevReserveBalances[1] - registry.getReserveAmount(address(usdToken)) + ); + assertEq( + newRegistry.getReserveAmount(address(weth)), + prevReserveBalances[2] - registry.getReserveAmount(address(weth)) + ); + + // assert token have been transfered + assertEq(linkToken.balanceOf(address(newRegistry)), newRegistry.getBalance(linkUpkeepID)); + assertEq(usdToken.balanceOf(address(newRegistry)), newRegistry.getBalance(usdUpkeepID)); + assertEq(weth.balanceOf(address(newRegistry)), newRegistry.getBalance(nativeUpkeepID)); + assertEq(linkToken.balanceOf(address(registry)), prevTokenBalances[0] - linkToken.balanceOf(address(newRegistry))); + assertEq(usdToken.balanceOf(address(registry)), prevTokenBalances[1] - usdToken.balanceOf(address(newRegistry))); + assertEq(weth.balanceOf(address(registry)), prevTokenBalances[2] - weth.balanceOf(address(newRegistry))); + + // assert upkeep data matches + assertEq(prevUpkeepData[0], abi.encode(newRegistry.getUpkeep(linkUpkeepID))); + assertEq(prevUpkeepData[1], abi.encode(newRegistry.getUpkeep(usdUpkeepID))); + assertEq(prevUpkeepData[2], abi.encode(newRegistry.getUpkeep(nativeUpkeepID))); + assertEq(prevUpkeepTriggerData[0], newRegistry.getUpkeepTriggerConfig(linkUpkeepID)); + assertEq(prevUpkeepTriggerData[1], newRegistry.getUpkeepTriggerConfig(usdUpkeepID)); + assertEq(prevUpkeepTriggerData[2], newRegistry.getUpkeepTriggerConfig(nativeUpkeepID)); + + vm.stopPrank(); + } +} diff --git a/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol b/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol index 49a4739645..eae07a8bab 100644 --- a/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol +++ b/contracts/src/v0.8/automation/dev/test/BaseTest.t.sol @@ -7,6 +7,7 @@ import {LinkToken} from "../../../shared/token/ERC677/LinkToken.sol"; import {ERC20Mock} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/mocks/ERC20Mock.sol"; import {MockV3Aggregator} from "../../../tests/MockV3Aggregator.sol"; import {AutomationForwarderLogic} from "../../AutomationForwarderLogic.sol"; +import {UpkeepTranscoder5_0 as Transcoder} from "../v2_3/UpkeepTranscoder5_0.sol"; import {AutomationRegistry2_3} from "../v2_3/AutomationRegistry2_3.sol"; import {AutomationRegistryBase2_3 as AutoBase} from "../v2_3/AutomationRegistryBase2_3.sol"; import {AutomationRegistryLogicA2_3} from "../v2_3/AutomationRegistryLogicA2_3.sol"; @@ -47,6 +48,7 @@ contract BaseTest is Test { MockV3Aggregator internal FAST_GAS_FEED; MockUpkeep internal TARGET1; MockUpkeep internal TARGET2; + Transcoder internal TRANSCODER; // roles address internal constant OWNER = address(uint160(uint256(keccak256("OWNER")))); @@ -82,6 +84,8 @@ contract BaseTest is Test { TARGET1 = new MockUpkeep(); TARGET2 = new MockUpkeep(); + TRANSCODER = new Transcoder(); + SIGNERS[0] = vm.addr(SIGNING_KEY0); //0xc110458BE52CaA6bB68E66969C3218A4D9Db0211 SIGNERS[1] = vm.addr(SIGNING_KEY1); //0xc110a19c08f1da7F5FfB281dc93630923F8E3719 SIGNERS[2] = vm.addr(SIGNING_KEY2); //0xc110fdF6e8fD679C7Cc11602d1cd829211A18e9b @@ -233,7 +237,7 @@ contract BaseTest is Test { fallbackGasPrice: 20_000_000_000, fallbackLinkPrice: 2_000_000_000, // $20 fallbackNativePrice: 400_000_000_000, // $4,000 - transcoder: 0xB1e66855FD67f6e85F0f0fA38cd6fBABdf00923c, + transcoder: address(TRANSCODER), registrars: registrars, upkeepPrivilegeManager: PRIVILEGE_MANAGER, chainModule: address(new ChainModuleBase()), @@ -265,6 +269,7 @@ contract BaseTest is Test { (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState(); AutomationRegistryBase2_3.OnchainConfig memory config = registry.getConfig(); address[] memory billingTokens = registry.getBillingTokens(); + AutomationRegistryBase2_3.BillingConfig[] memory billingTokenConfigs = new AutomationRegistryBase2_3.BillingConfig[](billingTokens.length); @@ -291,6 +296,38 @@ contract BaseTest is Test { ); } + /// @notice this function removes a billing token from the registry + function _removeBillingTokenConfig(Registry registry, address billingToken) internal { + (, , address[] memory signers, address[] memory transmitters, uint8 f) = registry.getState(); + AutomationRegistryBase2_3.OnchainConfig memory config = registry.getConfig(); + address[] memory billingTokens = registry.getBillingTokens(); + + address[] memory newBillingTokens = new address[](billingTokens.length - 1); + AutomationRegistryBase2_3.BillingConfig[] + memory billingTokenConfigs = new AutomationRegistryBase2_3.BillingConfig[](billingTokens.length - 1); + + uint256 j = 0; + for (uint256 i = 0; i < billingTokens.length; i++) { + if (billingTokens[i] != billingToken) { + if (j == newBillingTokens.length) revert("could not find billing token provided on registry"); + newBillingTokens[j] = billingTokens[i]; + billingTokenConfigs[j] = registry.getBillingTokenConfig(billingTokens[i]); + j++; + } + } + + registry.setConfigTypeSafe( + signers, + transmitters, + f, + config, + OFFCHAIN_CONFIG_VERSION, + "", + newBillingTokens, + billingTokenConfigs + ); + } + function _transmit(uint256 id, Registry registry) internal { uint256[] memory ids = new uint256[](1); ids[0] = id; diff --git a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol index 35168133e1..22753cc4ac 100644 --- a/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol +++ b/contracts/src/v0.8/automation/dev/v2_3/AutomationRegistryLogicA2_3.sol @@ -146,7 +146,6 @@ contract AutomationRegistryLogicA2_3 is AutomationRegistryBase2_3, Chainable { * @dev migration permissions must be set on *both* sending and receiving registries * @dev only an upkeep admin can migrate their upkeeps * @dev this function is most gas-efficient if upkeepIDs are sorted by billing token - * @dev TODO - this needs better multi-token testing */ function migrateUpkeeps(uint256[] calldata ids, address destination) external { if ( diff --git a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts index a12c5812c7..1036ab2ce8 100644 --- a/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts +++ b/contracts/test/v0.8/automation/AutomationRegistry2_3.test.ts @@ -4953,173 +4953,6 @@ describe('AutomationRegistry2_3', () => { }) }) - describe('#migrateUpkeeps() / #receiveUpkeeps()', async () => { - context('when permissions are set', () => { - beforeEach(async () => { - await linkToken.connect(owner).approve(registry.address, toWei('100')) - await registry.connect(owner).addFunds(upkeepId, toWei('100')) - await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 1) - await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 2) - }) - - it('migrates an upkeep', async () => { - const offchainBytes = '0x987654abcd' - await registry - .connect(admin) - .setUpkeepOffchainConfig(upkeepId, offchainBytes) - const reg1Upkeep = await registry.getUpkeep(upkeepId) - const forwarderAddress = await registry.getForwarder(upkeepId) - expect(reg1Upkeep.balance).to.equal(toWei('100')) - expect(reg1Upkeep.checkData).to.equal(randomBytes) - expect(forwarderAddress).to.not.equal(ethers.constants.AddressZero) - expect(reg1Upkeep.offchainConfig).to.equal(offchainBytes) - expect((await registry.getState()).state.numUpkeeps).to.equal( - numUpkeeps, - ) - const forwarder = IAutomationForwarderFactory.connect( - forwarderAddress, - owner, - ) - expect(await forwarder.getRegistry()).to.equal(registry.address) - // Set an upkeep admin transfer in progress too - await registry - .connect(admin) - .transferUpkeepAdmin(upkeepId, await payee1.getAddress()) - - // migrate - await registry - .connect(admin) - .migrateUpkeeps([upkeepId], mgRegistry.address) - expect((await registry.getState()).state.numUpkeeps).to.equal( - numUpkeeps - 1, - ) - expect((await mgRegistry.getState()).state.numUpkeeps).to.equal(1) - expect((await registry.getUpkeep(upkeepId)).balance).to.equal(0) - expect((await registry.getUpkeep(upkeepId)).checkData).to.equal('0x') - expect((await mgRegistry.getUpkeep(upkeepId)).balance).to.equal( - toWei('100'), - ) - expect(await mgRegistry.getReserveAmount(linkToken.address)).to.equal( - toWei('100'), - ) - expect((await mgRegistry.getUpkeep(upkeepId)).checkData).to.equal( - randomBytes, - ) - expect((await mgRegistry.getUpkeep(upkeepId)).offchainConfig).to.equal( - offchainBytes, - ) - expect(await mgRegistry.getForwarder(upkeepId)).to.equal( - forwarderAddress, - ) - // test that registry is updated on forwarder - expect(await forwarder.getRegistry()).to.equal(mgRegistry.address) - // migration will delete the upkeep and nullify admin transfer - await expect( - registry.connect(payee1).acceptUpkeepAdmin(upkeepId), - ).to.be.revertedWithCustomError(registry, 'UpkeepCancelled') - await expect( - mgRegistry.connect(payee1).acceptUpkeepAdmin(upkeepId), - ).to.be.revertedWithCustomError( - mgRegistry, - 'OnlyCallableByProposedAdmin', - ) - }) - - it('migrates a paused upkeep', async () => { - expect((await registry.getUpkeep(upkeepId)).balance).to.equal( - toWei('100'), - ) - expect((await registry.getUpkeep(upkeepId)).checkData).to.equal( - randomBytes, - ) - expect((await registry.getState()).state.numUpkeeps).to.equal( - numUpkeeps, - ) - await registry.connect(admin).pauseUpkeep(upkeepId) - // verify the upkeep is paused - expect((await registry.getUpkeep(upkeepId)).paused).to.equal(true) - // migrate - await registry - .connect(admin) - .migrateUpkeeps([upkeepId], mgRegistry.address) - expect((await registry.getState()).state.numUpkeeps).to.equal( - numUpkeeps - 1, - ) - expect((await mgRegistry.getState()).state.numUpkeeps).to.equal(1) - expect((await registry.getUpkeep(upkeepId)).balance).to.equal(0) - expect((await mgRegistry.getUpkeep(upkeepId)).balance).to.equal( - toWei('100'), - ) - expect((await registry.getUpkeep(upkeepId)).checkData).to.equal('0x') - expect((await mgRegistry.getUpkeep(upkeepId)).checkData).to.equal( - randomBytes, - ) - expect(await mgRegistry.getReserveAmount(linkToken.address)).to.equal( - toWei('100'), - ) - // verify the upkeep is still paused after migration - expect((await mgRegistry.getUpkeep(upkeepId)).paused).to.equal(true) - }) - - it('emits an event on both contracts', async () => { - expect((await registry.getUpkeep(upkeepId)).balance).to.equal( - toWei('100'), - ) - expect((await registry.getUpkeep(upkeepId)).checkData).to.equal( - randomBytes, - ) - expect((await registry.getState()).state.numUpkeeps).to.equal( - numUpkeeps, - ) - const tx = registry - .connect(admin) - .migrateUpkeeps([upkeepId], mgRegistry.address) - await expect(tx) - .to.emit(registry, 'UpkeepMigrated') - .withArgs(upkeepId, toWei('100'), mgRegistry.address) - await expect(tx) - .to.emit(mgRegistry, 'UpkeepReceived') - .withArgs(upkeepId, toWei('100'), registry.address) - }) - - it('is only migratable by the admin', async () => { - await expect( - registry - .connect(owner) - .migrateUpkeeps([upkeepId], mgRegistry.address), - ).to.be.revertedWithCustomError(registry, 'OnlyCallableByAdmin') - await registry - .connect(admin) - .migrateUpkeeps([upkeepId], mgRegistry.address) - }) - }) - - context('when permissions are not set', () => { - it('reverts', async () => { - // no permissions - await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 0) - await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 0) - await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to - .be.reverted - // only outgoing permissions - await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 1) - await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 0) - await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to - .be.reverted - // only incoming permissions - await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 0) - await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 2) - await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to - .be.reverted - // permissions opposite direction - await registry.setPeerRegistryMigrationPermission(mgRegistry.address, 2) - await mgRegistry.setPeerRegistryMigrationPermission(registry.address, 1) - await expect(registry.migrateUpkeeps([upkeepId], mgRegistry.address)).to - .be.reverted - }) - }) - }) - describe('#setPayees', () => { const IGNORE_ADDRESS = '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF'