Skip to content

Commit

Permalink
AUTO-9179: settle NOPs LINK payment offchain (#12469)
Browse files Browse the repository at this point in the history
* AUTO-9179: settle NOPs LINK payment offchain

* update tests

* update tests

* update

* add tests

* update based on comments

* revoke lock file

* update

* udpate tests

* format

* small fixes

* format again

* update event sig
  • Loading branch information
FelixFan1992 authored Mar 20, 2024
1 parent 627c800 commit 1370133
Show file tree
Hide file tree
Showing 19 changed files with 1,055 additions and 114 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-rabbits-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

implement offchain settlement for NOPs payment
5 changes: 5 additions & 0 deletions contracts/.changeset/lazy-sheep-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chainlink/contracts": minor
---

implement offchain settlement for NOPs payment

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {BaseTest} from "./BaseTest.t.sol";
import {IAutomationRegistryMaster2_3} from "../interfaces/v2_3/IAutomationRegistryMaster2_3.sol";
import {AutomationRegistrar2_3} from "../v2_3/AutomationRegistrar2_3.sol";
import {IERC20} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {AutomationRegistryBase2_3 as AutoBase} from "../v2_3/AutomationRegistryBase2_3.sol";

// forge test --match-path src/v0.8/automation/dev/test/AutomationRegistrar2_3.t.sol

Expand All @@ -14,7 +15,7 @@ contract SetUp is BaseTest {

function setUp() public override {
super.setUp();
(registry, registrar) = deployAndConfigureAll();
(registry, registrar) = deployAndConfigureAll(AutoBase.PayoutMode.ON_CHAIN);
vm.stopPrank(); // reset identity at the start of each test
}
}
Expand Down
223 changes: 207 additions & 16 deletions contracts/src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@
pragma solidity 0.8.19;

import {BaseTest} from "./BaseTest.t.sol";
import {AutomationRegistryBase2_3 as AutoBase} from "../v2_3/AutomationRegistryBase2_3.sol";
import {IAutomationRegistryMaster2_3, AutomationRegistryBase2_3} from "../interfaces/v2_3/IAutomationRegistryMaster2_3.sol";
import {ChainModuleBase} from "../../chains/ChainModuleBase.sol";
import {IAutomationV21PlusCommon} from "../../interfaces/IAutomationV21PlusCommon.sol";

// forge test --match-path src/v0.8/automation/dev/test/AutomationRegistry2_3.t.sol

contract SetUp is BaseTest {
address[] internal s_registrars;

IAutomationRegistryMaster2_3 internal registry;
uint256[] internal upkeepIds;
uint256[] internal gasLimits;
bytes[] internal performDatas;
uint256[] internal balances;

function setUp() public virtual override {
super.setUp();

s_registrars = new address[](1);
s_registrars[0] = 0x3a0eDE26aa188BFE00b9A0C9A431A1a0CA5f7966;

(registry, ) = deployAndConfigureAll();
(registry, ) = deployAndConfigureAll(AutoBase.PayoutMode.ON_CHAIN);
}
}

Expand Down Expand Up @@ -46,9 +52,43 @@ contract CheckUpkeep is SetUp {
contract Withdraw is SetUp {
address internal aMockAddress = address(0x1111111111111111111111111111111111111113);

function setConfigForWithdraw() public {
address module = address(new ChainModuleBase());
AutomationRegistryBase2_3.OnchainConfig memory cfg = AutomationRegistryBase2_3.OnchainConfig({
checkGasLimit: 5_000_000,
stalenessSeconds: 90_000,
gasCeilingMultiplier: 0,
maxPerformGas: 10_000_000,
maxCheckDataSize: 5_000,
maxPerformDataSize: 5_000,
maxRevertDataSize: 5_000,
fallbackGasPrice: 20_000_000_000,
fallbackLinkPrice: 2_000_000_000, // $20
fallbackNativePrice: 400_000_000_000, // $4,000
transcoder: 0xB1e66855FD67f6e85F0f0fA38cd6fBABdf00923c,
registrars: s_registrars,
upkeepPrivilegeManager: 0xD9c855F08A7e460691F41bBDDe6eC310bc0593D8,
chainModule: module,
reorgProtectionEnabled: true,
financeAdmin: FINANCE_ADMIN
});
bytes memory offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS);

registry.setConfigTypeSafe(
SIGNERS,
TRANSMITTERS,
F,
cfg,
OFFCHAIN_CONFIG_VERSION,
offchainConfigBytes,
new address[](0),
new AutomationRegistryBase2_3.BillingConfig[](0)
);
}

function testLinkAvailableForPaymentReturnsLinkBalance() public {
//simulate a deposit of link to the liquidity pool
mintLink(address(registry), 1e10);
_mintLink(address(registry), 1e10);

//check there's a balance
assertGt(linkToken.balanceOf(address(registry)), 0);
Expand Down Expand Up @@ -84,7 +124,7 @@ contract Withdraw is SetUp {

function testWithdrawLinkFeeSuccess() public {
//simulate a deposit of link to the liquidity pool
mintLink(address(registry), 1e10);
_mintLink(address(registry), 1e10);

//check there's a balance
assertGt(linkToken.balanceOf(address(registry)), 0);
Expand All @@ -102,7 +142,7 @@ contract Withdraw is SetUp {

function testWithdrawERC20FeeSuccess() public {
// simulate a deposit of ERC20 to the liquidity pool
mintERC20(address(registry), 1e10);
_mintERC20(address(registry), 1e10);

// check there's a balance
assertGt(mockERC20.balanceOf(address(registry)), 0);
Expand Down Expand Up @@ -173,9 +213,7 @@ contract SetConfig is SetUp {
bytes memory onchainConfigBytes = abi.encode(cfg);
bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs);

uint256 a = 1234;
address b = ZERO_ADDRESS;
bytes memory offchainConfigBytes = abi.encode(a, b);
bytes memory offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS);
bytes32 configDigest = _configDigestFromConfigData(
block.chainid,
address(registry),
Expand Down Expand Up @@ -254,9 +292,7 @@ contract SetConfig is SetUp {

bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs);

uint256 a = 1234;
address b = ZERO_ADDRESS;
bytes memory offchainConfigBytes = abi.encode(a, b);
bytes memory offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS);

registry.setConfig(
SIGNERS,
Expand Down Expand Up @@ -327,9 +363,7 @@ contract SetConfig is SetUp {

bytes memory onchainConfigBytesWithBilling2 = abi.encode(cfg, billingTokens2, billingConfigs2);

uint256 a = 1234;
address b = ZERO_ADDRESS;
bytes memory offchainConfigBytes = abi.encode(a, b);
bytes memory offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS);

// set config once
registry.setConfig(
Expand Down Expand Up @@ -396,9 +430,7 @@ contract SetConfig is SetUp {

bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs);

uint256 a = 1234;
address b = ZERO_ADDRESS;
bytes memory offchainConfigBytes = abi.encode(a, b);
bytes memory offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS);

// expect revert because of duplicate tokens
vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.DuplicateEntry.selector));
Expand All @@ -412,6 +444,35 @@ contract SetConfig is SetUp {
);
}

function testSetConfigRevertDueToInvalidBillingToken() public {
address[] memory billingTokens = new address[](1);
billingTokens[0] = address(linkToken);

AutomationRegistryBase2_3.BillingConfig[] memory billingConfigs = new AutomationRegistryBase2_3.BillingConfig[](1);
billingConfigs[0] = AutomationRegistryBase2_3.BillingConfig({
gasFeePPB: 5_000,
flatFeeMicroLink: 20_000,
priceFeed: 0x2222222222222222222222222222222222222222,
fallbackPrice: 2_000_000_000, // $20
minSpend: 100_000
});

bytes memory onchainConfigBytesWithBilling = abi.encode(cfg, billingTokens, billingConfigs);
bytes memory offchainConfigBytes = abi.encode(1234, ZERO_ADDRESS);
// deploy registry with OFF_CHAIN payout mode
registry = deployRegistry(AutoBase.PayoutMode.OFF_CHAIN);

vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.InvalidBillingToken.selector));
registry.setConfig(
SIGNERS,
TRANSMITTERS,
F,
onchainConfigBytesWithBilling,
OFFCHAIN_CONFIG_VERSION,
offchainConfigBytes
);
}

function _configDigestFromConfigData(
uint256 chainId,
address contractAddress,
Expand Down Expand Up @@ -443,3 +504,133 @@ contract SetConfig is SetUp {
return bytes32((prefix & prefixMask) | (h & ~prefixMask));
}
}

contract NOPsSettlement is SetUp {
event NOPsSettledOffchain(address[] payees, uint256[] balances);

function testSettleNOPsOffchainRevertDueToUnauthorizedCaller() public {
(IAutomationRegistryMaster2_3 registry, ) = deployAndConfigureAll(AutoBase.PayoutMode.ON_CHAIN);

vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.OnlyFinanceAdmin.selector));
registry.settleNOPsOffchain();
}

function testSettleNOPsOffchainRevertDueToOffchainSettlementDisabled() public {
(IAutomationRegistryMaster2_3 registry, ) = deployAndConfigureAll(AutoBase.PayoutMode.OFF_CHAIN);

vm.prank(registry.owner());
registry.disableOffchainPayments();

vm.prank(FINANCE_ADMIN);
vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.MustSettleOnchain.selector));
registry.settleNOPsOffchain();
}

function testSettleNOPsOffchainSuccess() public {
// deploy and configure a registry with OFF_CHAIN payout
(IAutomationRegistryMaster2_3 registry, ) = deployAndConfigureAll(AutoBase.PayoutMode.OFF_CHAIN);
registry.setPayees(PAYEES);

uint256[] memory balances = new uint256[](TRANSMITTERS.length);
for (uint256 i = 0; i < TRANSMITTERS.length; i++) {
balances[i] = 0;
}

vm.startPrank(FINANCE_ADMIN);
vm.expectEmit();
emit NOPsSettledOffchain(PAYEES, balances);
registry.settleNOPsOffchain();
}

function testSettleNOPsOffchainSuccessTransmitterBalanceZeroed() public {
// deploy and configure a registry with OFF_CHAIN payout
(IAutomationRegistryMaster2_3 registry, ) = deployAndConfigureAll(AutoBase.PayoutMode.OFF_CHAIN);
registry.setPayees(PAYEES);

// register an upkeep and add funds
uint256 id = registry.registerUpkeep(address(TARGET1), 1000000, UPKEEP_ADMIN, 0, address(mockERC20), "", "", "");
_mintERC20(UPKEEP_ADMIN, 1e20);
vm.startPrank(UPKEEP_ADMIN);
mockERC20.approve(address(registry), 1e20);
registry.addFunds(id, 1e20);

// manually create a transmit so transmitters earn some rewards
upkeepIds = new uint256[](1);
gasLimits = new uint256[](1);
performDatas = new bytes[](1);
bytes[] memory triggers = new bytes[](1);
upkeepIds[0] = id;
gasLimits[0] = 1000000;
triggers[0] = _encodeConditionalTrigger(
AutoBase.ConditionalTrigger(uint32(block.number - 1), blockhash(block.number - 1))
);
AutoBase.Report memory report = AutoBase.Report(
uint256(1000000000),
uint256(2000000000),
upkeepIds,
gasLimits,
triggers,
performDatas
);
bytes memory reportBytes = _encodeReport(report);
(, , bytes32 configDigest) = registry.latestConfigDetails();
bytes32[3] memory reportContext = [configDigest, configDigest, configDigest];
uint256[] memory signerPKs = new uint256[](2);
signerPKs[0] = SIGNING_KEY0;
signerPKs[1] = SIGNING_KEY1;
(bytes32[] memory rs, bytes32[] memory ss, bytes32 vs) = _signReport(reportBytes, reportContext, signerPKs);

vm.startPrank(TRANSMITTERS[0]);
registry.transmit(reportContext, reportBytes, rs, ss, vs);

// verify transmitters have positive balances
(bool active, uint8 index, uint96 balance, uint96 lastCollected, ) = registry.getTransmitterInfo(TRANSMITTERS[1]);
assertTrue(active);
assertEq(1, index);
assertTrue(balance > 0);
assertEq(0, lastCollected);

balances = new uint256[](TRANSMITTERS.length);
for (uint256 i = 0; i < balances.length; i++) {
balances[i] = balance;
}

// verify offchain settlement will emit NOPs' balances
vm.startPrank(FINANCE_ADMIN);
vm.expectEmit();
emit NOPsSettledOffchain(PAYEES, balances);
registry.settleNOPsOffchain();

// verify that transmitters balance has been zeroed out
(active, index, balance, , ) = registry.getTransmitterInfo(TRANSMITTERS[2]);
assertTrue(active);
assertEq(2, index);
assertEq(0, balance);
}

function testDisableOffchainPaymentsRevertDueToUnauthorizedCaller() public {
(IAutomationRegistryMaster2_3 registry, ) = deployAndConfigureAll(AutoBase.PayoutMode.OFF_CHAIN);

vm.startPrank(FINANCE_ADMIN);
vm.expectRevert(bytes("Only callable by owner"));
registry.disableOffchainPayments();
}

function testDisableOffchainPaymentsSuccess() public {
(IAutomationRegistryMaster2_3 registry, ) = deployAndConfigureAll(AutoBase.PayoutMode.OFF_CHAIN);

vm.startPrank(registry.owner());
registry.disableOffchainPayments();

assertEq(uint8(AutoBase.PayoutMode.ON_CHAIN), registry.getPayoutMode());
}
}

contract WithdrawPayment is SetUp {
function testWithdrawPaymentRevertDueToOffchainPayoutMode() public {
registry = deployRegistry(AutoBase.PayoutMode.OFF_CHAIN);
vm.expectRevert(abi.encodeWithSelector(IAutomationRegistryMaster2_3.MustSettleOffchain.selector));
vm.prank(TRANSMITTERS[0]);
registry.withdrawPayment(TRANSMITTERS[0], TRANSMITTERS[0]);
}
}
Loading

0 comments on commit 1370133

Please sign in to comment.