Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change KeystoneForwarder routing gas accounting #14543

Merged
merged 13 commits into from
Sep 25, 2024
Merged
5 changes: 5 additions & 0 deletions .changeset/long-balloons-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

#internal
5 changes: 5 additions & 0 deletions contracts/.changeset/proud-pears-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/contracts': patch
---

#internal
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity 0.8.24;

import {BaseTest} from "./KeystoneForwarderBaseTest.t.sol";
import {IRouter} from "../interfaces/IRouter.sol";
import {MaliciousInterfaceReceiver} from "./mocks/MaliciousInterfaceReceiver.sol";
import {MaliciousReportReceiver} from "./mocks/MaliciousReportReceiver.sol";
import {KeystoneForwarder} from "../KeystoneForwarder.sol";

contract KeystoneForwarder_ReportTest is BaseTest {
Expand Down Expand Up @@ -236,6 +238,42 @@ contract KeystoneForwarder_ReportTest is BaseTest {
assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.INVALID_RECEIVER), "state mismatch");
}

function test_Report_FailedDelieryWhenReportReceiverConsumesAllGas() public {
MaliciousReportReceiver s_maliciousReceiver = new MaliciousReportReceiver();
s_forwarder.report{gas: 500_000}(address(s_maliciousReceiver), report, reportContext, signatures);

IRouter.TransmissionInfo memory transmissionInfo = s_forwarder.getTransmissionInfo(
address(s_maliciousReceiver),
executionId,
reportId
);

assertEq(transmissionInfo.transmitter, TRANSMITTER, "transmitter mismatch");
assertEq(uint8(transmissionInfo.state), uint8(IRouter.TransmissionState.FAILED), "state mismatch");
assertGt(transmissionInfo.gasLimit, 430_000, "gas limit mismatch");
}

function test_Report_FailedDelieryWhenInterfaceCheckConsumesAllGas() public {
uint256 gasProvided = 550_000;
uint256 expectedGasLimit = 400_000;
MaliciousInterfaceReceiver s_maliciousReceiver = new MaliciousInterfaceReceiver(expectedGasLimit);
s_forwarder.report{gas: gasProvided}(address(s_maliciousReceiver), report, reportContext, signatures);

IRouter.TransmissionInfo memory transmissionInfo = s_forwarder.getTransmissionInfo(
address(s_maliciousReceiver),
executionId,
reportId
);

assertEq(transmissionInfo.transmitter, TRANSMITTER, "transmitter mismatch");
assertGt(transmissionInfo.gasLimit, expectedGasLimit, "expected gas limit was not provided");
assertEq(
uint8(transmissionInfo.state),
uint8(IRouter.TransmissionState.SUCCEEDED),
"state does not match SUCCEEDED"
);
}

function test_Report_ConfigVersion() public {
vm.stopPrank();
// configure a new configVersion
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol";
import {IReceiver} from "../../interfaces/IReceiver.sol";

contract MaliciousInterfaceReceiver is IReceiver, IERC165 {
error InsufficientGasProvided();
event GasProvided(uint256 gasProvided, uint256 gasExpected, bool sufficient);
event MessageReceived(bytes metadata, bytes[] mercuryReports);
bytes public latestReport;
uint256 internal s_expectedGasLimit;

constructor(uint256 expectedGasLimit) {
s_expectedGasLimit = expectedGasLimit;
}

function onReport(bytes calldata, bytes calldata) external {
uint256 providedGas = gasleft();
emit GasProvided(providedGas, s_expectedGasLimit, providedGas >= s_expectedGasLimit);

if (providedGas < s_expectedGasLimit) {
revert InsufficientGasProvided();
}
}

function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
// Consume up to the maximum amount of gas that can be consumed in this check
// This loop consumes roughly 29_000 gas
for (uint256 i = 0; i < 670; i++) {}

return interfaceId == type(IReceiver).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {IERC165} from "../../../vendor/openzeppelin-solidity/v4.8.3/contracts/interfaces/IERC165.sol";
import {IReceiver} from "../../interfaces/IReceiver.sol";

contract MaliciousReportReceiver is IReceiver, IERC165 {
event MessageReceived(bytes metadata, bytes[] mercuryReports);
bytes public latestReport;

function onReport(bytes calldata metadata, bytes calldata rawReport) external {
// Exhaust all gas that was provided
for (uint256 i = 0; i < 1_000_000_000; i++) {
bytes[] memory mercuryReports = abi.decode(rawReport, (bytes[]));
latestReport = rawReport;
emit MessageReceived(metadata, mercuryReports);
}
}

function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
return interfaceId == this.onReport.selector;
}
}
8 changes: 4 additions & 4 deletions core/capabilities/targets/write_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
)

var (
_ capabilities.ActionCapability = &WriteTarget{}
_ capabilities.TargetCapability = &WriteTarget{}
)

type WriteTarget struct {
Expand Down Expand Up @@ -48,7 +48,7 @@ type TransmissionInfo struct {
// The gas cost of the forwarder contract logic, including state updates and event emission.
// This is a rough estimate and should be updated if the forwarder contract logic changes.
// TODO: Make this part of the on-chain capability configuration
const FORWARDER_CONTRACT_LOGIC_GAS_COST = 100_000
const ForwarderContractLogicGasCost = 150_000

type ContractValueGetter interface {
Bind(context.Context, []commontypes.BoundContract) error
Expand Down Expand Up @@ -77,7 +77,7 @@ func NewWriteTarget(
Name: "forwarder",
},
forwarderAddress,
txGasLimit - FORWARDER_CONTRACT_LOGIC_GAS_COST,
txGasLimit - ForwarderContractLogicGasCost,
info,
logger.Named(lggr, "WriteTarget"),
false,
Expand Down Expand Up @@ -252,7 +252,7 @@ func (cap *WriteTarget) Execute(ctx context.Context, rawRequest capabilities.Cap
case transmissionInfo.State == 3: // FAILED
receiverGasMinimum := cap.receiverGasMinimum
if request.Config.GasLimit != nil {
receiverGasMinimum = *request.Config.GasLimit - FORWARDER_CONTRACT_LOGIC_GAS_COST
receiverGasMinimum = *request.Config.GasLimit - ForwarderContractLogicGasCost
}
if transmissionInfo.GasLimit.Uint64() > receiverGasMinimum {
cap.lggr.Infow("returning without a transmission attempt - transmission already attempted and failed, sufficient gas was provided", "executionID", request.Metadata.WorkflowExecutionID, "receiverGasMinimum", receiverGasMinimum, "transmissionGasLimit", transmissionInfo.GasLimit)
Expand Down
Loading