Skip to content

Commit

Permalink
[INTAUTO-272] Functions support zksync (#15991)
Browse files Browse the repository at this point in the history
* fix: point to 1.3.0 ocr

* chore: add functions router zksync to solhint ignore

* chore: add changeset

* chore: removed not modified contracts

* fix: remove l1 fee logic

* fix: unused vars

* Adds exact gas bound call implementation for zkSync

This commit adds a safe implementation for the gas bound call
implementation for zksync. It also adds a custom Functions router
contract for zkSync.

* Bump solidity version

* Typo fix

* Minor changes

* Adds solidity version 0.8.20

* Addressing comments

* Address feedback

* Address feedback

* Typo fix

* Replaced require statements with custom error and other solhint recomendations

* Downgrade shared solidity version to 0.8.20

* Set evm version to paris to fix tests

* Fix comment

* Auto detect shared solc version for shared

* Uses rawCall and vendors zksync dependency

* Adds tests and used near call pattern

* Downgrade hardhat

* Downgrade to solidity 0.8.19

* Adds zkSyncFunctionsRouter tests

* Adds gas snapshots

* Adds gas snapshot for shared

* Solhint fixes

* fix: call with exact gas

* Adds more tests and addresses comments

* Fixes comment and adjusts test

* Updates gas snapshot

* Adds pnpm lock file

* Modifies comment

* Modifies comment

* Addresses comments

* Updates gas snapshot

* build CL image also on keystone changes

---------

Co-authored-by: Kodey Kilday-Thomas <[email protected]>
Co-authored-by: Bartek Tofel <[email protected]>
Co-authored-by: Bartek Tofel <[email protected]>
  • Loading branch information
4 people authored Feb 19, 2025
1 parent 95fa633 commit ff814eb
Show file tree
Hide file tree
Showing 19 changed files with 1,507 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ jobs:
aws-region: ${{ secrets.AWS_REGION }}
set-git-config: "true"
- name: Build Chainlink Image
if: needs.changes.outputs.core_changes == 'true' || needs.changes.outputs.github_ci_changes == 'true' || github.event_name == 'workflow_dispatch'
if: needs.changes.outputs.core_changes == 'true' || needs.changes.outputs.keystone_changes == 'true' || needs.changes.outputs.github_ci_changes == 'true' || github.event_name == 'workflow_dispatch'
uses: ./.github/actions/build-chainlink-image
with:
tag_suffix: ${{ matrix.image.tag-suffix }}
Expand Down
5 changes: 5 additions & 0 deletions contracts/.changeset/loud-rabbits-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/contracts': patch
---

Added ZKSync support for Functions
2 changes: 1 addition & 1 deletion contracts/.solhintignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@
./node_modules/

# Ignore tweaked vendored contracts
./src/v0.8/shared/enumerable/EnumerableSetWithBytes16.sol
./src/v0.8/shared/enumerable/EnumerableSetWithBytes16.sol
11 changes: 6 additions & 5 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ optimizer_runs = 1_000_000
src = 'src/v0.8'
test = 'test/v0.8'
out = 'foundry-artifacts'
cache_path = 'foundry-cache'
cache_path = 'foundry-cache'
libs = ['node_modules']
bytecode_hash = "none"
ffi = false
Expand Down Expand Up @@ -42,9 +42,10 @@ via_ir = true

[profile.functions]
solc_version = '0.8.19'
src = 'src/v0.8/functions/dev/v1_X'
test = 'src/v0.8/functions/tests/v1_X'
gas_price = 3_000_000_000 # 3 gwei
evm_version = 'paris'
src = 'src/v0.8/functions'
test = 'src/v0.8/functions/tests'
gas_price = 3_000_000_000 # 3 gwei

[profile.vrf]
optimizer_runs = 1_000
Expand Down Expand Up @@ -101,7 +102,7 @@ optimizer_runs = 1_000_000
solc_version = '0.8.24'
src = 'src/v0.8/workflow'
test = 'src/v0.8/workflow/test'
via_ir = true # reconsider using the --via-ir flag if compilation takes too long
via_ir = true # reconsider using the --via-ir flag if compilation takes too long
evm_version = 'paris'

[profile.data-feeds]
Expand Down
5 changes: 4 additions & 1 deletion contracts/gas-snapshots/functions.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,7 @@ Gas_FulfillRequest_Success:test_FulfillRequest_Success_MaximumGas() (gas: 498189
Gas_FulfillRequest_Success:test_FulfillRequest_Success_MinimumGas() (gas: 199381)
Gas_FundSubscription:test_FundSubscription_Gas() (gas: 35545)
Gas_SendRequest:test_SendRequest_MaximumGas() (gas: 981365)
Gas_SendRequest:test_SendRequest_MinimumGas() (gas: 178588)
Gas_SendRequest:test_SendRequest_MinimumGas() (gas: 178588)
ZKSyncFunctionsRouter__Callback:test__callback_PubdataUsage_IsZero() (gas: 45769)
ZKSyncFunctionsRouter__Callback:test__callback_ReturnDataTruncation() (gas: 584931)
ZKSyncFunctionsRouter__Callback:test__callback_Success() (gas: 40113)
3 changes: 3 additions & 0 deletions contracts/gas-snapshots/shared.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ BurnMintERC677_mint:testSenderNotMinterReverts() (gas: 11195)
BurnMintERC677_supportsInterface:testConstructorSuccess() (gas: 12476)
BurnMintERC677_transfer:testInvalidAddressReverts() (gas: 10639)
BurnMintERC677_transfer:testTransferSuccess() (gas: 42299)
CallWithExactGasZKSync__callWithExactGasSafeReturnData:test__callWithExactGasSafeReturnData_FailsWhen_NotEnoughGasForCall() (gas: 18544)
CallWithExactGasZKSync__callWithExactGasSafeReturnData:test__callWithExactGasSafeReturnData_Success() (gas: 27871)
CallWithExactGasZKSync__callWithExactGasSafeReturnData:test__callWithExactGasSafeReturnData_TruncatesData() (gas: 75408)
CallWithExactGas__callWithExactGas:test_CallWithExactGasReceiverErrorSuccess() (gas: 65949)
CallWithExactGas__callWithExactGas:test_CallWithExactGasSafeReturnDataExactGas() (gas: 18324)
CallWithExactGas__callWithExactGas:test_NoContractReverts() (gas: 11559)
Expand Down
40 changes: 39 additions & 1 deletion contracts/src/v0.8/functions/tests/v1_X/Setup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity ^0.8.19;

import {BaseTest} from "./BaseTest.t.sol";
import {FunctionsClientHarness} from "./testhelpers/FunctionsClientHarness.sol";
import {ZKSyncFunctionsRouterHarness, ZKSyncFunctionsRouter} from "./testhelpers/ZKSyncFunctionsRouterHarness.sol";
import {FunctionsRouter as FunctionsRouterStable} from "../../v1_0_0/FunctionsRouter.sol";
import {FunctionsRouterHarness, FunctionsRouter} from "./testhelpers/FunctionsRouterHarness.sol";
import {FunctionsCoordinatorHarness} from "./testhelpers/FunctionsCoordinatorHarness.sol";
import {FunctionsBilling} from "../../dev/v1_X/FunctionsBilling.sol";
Expand Down Expand Up @@ -105,6 +107,42 @@ contract FunctionsRouterSetup is BaseTest {
}
}

/// @notice Set up to deploy the following contracts: FunctionsRouter, FunctionsCoordinator, LINK/ETH Feed, ToS Allow List, and LINK token
contract ZKSyncFunctionsRouterSetup is BaseTest {
ZKSyncFunctionsRouterHarness internal s_functionsRouter;
MockLinkToken internal s_linkToken;

uint16 internal s_maxConsumersPerSubscription = 3;
uint72 internal s_adminFee = 0; // Keep as 0. Setting this to anything else will cause fulfillments to fail with INVALID_COMMITMENT
bytes4 internal s_handleOracleFulfillmentSelector = 0x0ca76175;
uint16 s_subscriptionDepositMinimumRequests = 1;
uint72 s_subscriptionDepositJuels = 11 * JUELS_PER_LINK;

function setUp() public virtual override {
BaseTest.setUp();
s_linkToken = new MockLinkToken();
s_functionsRouter = new ZKSyncFunctionsRouterHarness(address(s_linkToken), this.getRouterConfig());
}

function getRouterConfig() public view returns (FunctionsRouterStable.Config memory) {
uint32[] memory maxCallbackGasLimits = new uint32[](3);
maxCallbackGasLimits[0] = 300_000;
maxCallbackGasLimits[1] = 500_000;
maxCallbackGasLimits[2] = 1_000_000;

return
FunctionsRouterStable.Config({
maxConsumersPerSubscription: s_maxConsumersPerSubscription,
adminFee: s_adminFee,
handleOracleFulfillmentSelector: s_handleOracleFulfillmentSelector,
maxCallbackGasLimits: maxCallbackGasLimits,
gasForCallExactCheck: 5000,
subscriptionDepositMinimumRequests: s_subscriptionDepositMinimumRequests,
subscriptionDepositJuels: s_subscriptionDepositJuels
});
}
}

/// @notice Set up to set the OCR configuration of the Coordinator contract
contract FunctionsDONSetup is FunctionsRouterSetup {
uint256 internal NOP_SIGNER_PRIVATE_KEY_1 = 0x400;
Expand Down Expand Up @@ -176,7 +214,7 @@ contract FunctionsDONSetup is FunctionsRouterSetup {
];
}

function _assertTransmittersAllHaveBalance(uint256[4] memory balances, uint256 expectedBalance) internal {
function _assertTransmittersAllHaveBalance(uint256[4] memory balances, uint256 expectedBalance) internal pure {
assertEq(balances[0], expectedBalance);
assertEq(balances[1], expectedBalance);
assertEq(balances[2], expectedBalance);
Expand Down
195 changes: 195 additions & 0 deletions contracts/src/v0.8/functions/tests/v1_X/ZKSyncFunctionsRouter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {BaseTest} from "./BaseTest.t.sol";
import {ZKSyncFunctionsRouter} from "../../v1_3_0_zksync/ZKSyncFunctionsRouter.sol";
import {FunctionsRouter} from "../../v1_0_0/FunctionsRouter.sol";
import {ZKSyncFunctionsRouterHarness} from "./testhelpers/ZKSyncFunctionsRouterHarness.sol";
import {ZKSyncFunctionsRouterSetup} from "./Setup.t.sol";
import {MockSystemContext} from "../../../shared/test/mocks/MockSystemContext.sol";

contract ZKSyncFunctionsRouter__Callback is ZKSyncFunctionsRouterSetup {
MockClientSuccess internal s_mockClientSuccess;
MockClientRevert internal s_mockClientRevert;
MockSystemContext internal s_mockSystemContext;

struct CallbackResult {
bool success;
uint256 gasUsed;
bytes returnData;
}

function setUp() public virtual override {
super.setUp();
s_mockClientSuccess = new MockClientSuccess();
s_mockClientRevert = new MockClientRevert();

s_mockSystemContext = new MockSystemContext();
// Write mock's code to 0x800b so that library calls see it
vm.etch(address(0x800b), address(s_mockSystemContext).code);
}

function test__callback_RevertWhen_NoClientCode() public {
bytes32 reqId = bytes32("reqIdNoCode");
bytes memory resp = bytes("responseData");
bytes memory err = bytes("errData");
uint32 totalGas = 5_000_000;
uint32 callbackGasLimit = 4_000_000;
address noCodeAddress = address(12345);

ZKSyncFunctionsRouter.CallbackResult memory result = _callback(
reqId,
resp,
err,
totalGas,
callbackGasLimit,
noCodeAddress
);

assertFalse(result.success, "Should skip => success=false");
assertEq(result.gasUsed, 0, "gasUsed=0 for skip");
assertEq(result.returnData.length, 0, "no return data");
}

function test__callback_Success() public {
bytes32 reqId = bytes32("reqSuccess");
bytes memory resp = bytes("responseData");
bytes memory err = bytes("errData");
uint32 totalGas = 5_000_000;
uint32 callbackGasLimit = 4_000_000;
address client = address(s_mockClientSuccess);

ZKSyncFunctionsRouter.CallbackResult memory result = _callback(
reqId,
resp,
err,
totalGas,
callbackGasLimit,
client
);

assertTrue(result.success, "callback should succeed");
assertGt(result.gasUsed, 0, "some gas used");
assertTrue(result.returnData.length > 0, "client returns a bool => should have data");
}

function test__callback_RevertWhen_ClientReverts() public {
bytes32 reqId = bytes32("reqIdRevert");
bytes memory resp = bytes("someResponse");
bytes memory err = bytes("someErr");
// Use a moderate gas limit so that we don't trigger the _maxTotalGas check.
uint32 totalGas = 5_000_000;
uint32 callbackGasLimit = 4_000_000;
address client = address(s_mockClientRevert);
ZKSyncFunctionsRouter.CallbackResult memory result = _callback(
reqId,
resp,
err,
totalGas,
callbackGasLimit,
client
);

assertFalse(result.success, "client revert => success=false");
assertGt(result.gasUsed, 0, "some gas is consumed");
// returnData should contain the revert reason "MockClientRevert"
assertTrue(result.returnData.length > 0, "contains revert reason data");
}

/// @notice Example test verifying pubdata usage is zero and comparing internal measurement with external gas usage.
function test__callback_PubdataUsage_IsZero() public {
s_mockSystemContext.setGasPerPubdataByte(0);
bytes32 reqId = bytes32("reqPubdata");
bytes memory resp = bytes("someResponse");
bytes memory err = bytes("someErr");
uint32 totalGas = 5_000_000;
uint32 callbackGasLimit = 4_000_000;
address client = address(s_mockClientSuccess);
uint256 startGas = gasleft();
ZKSyncFunctionsRouter.CallbackResult memory result = _callback(
reqId,
resp,
err,
totalGas,
callbackGasLimit,
client
);
uint256 endGas = gasleft();
uint256 actualUsed = startGas - endGas;
assertTrue(result.success, "callback success");
assertGt(result.gasUsed, 0, "callback claims >0 gas used");
// Allow a margin between the router's internal measurement and actual external usage.
assertLe(result.gasUsed, actualUsed, "Router's gasUsed should not exceed actual external usage by large margin");
}

/// @notice Confirm large return data gets truncated by _maxReturnBytes.
function test__callback_ReturnDataTruncation() public {
// Deploy large-return client.
MockClientLargeReturn bigClient = new MockClientLargeReturn();

bytes32 reqId = bytes32("reqLargeReturn");
bytes memory resp = bytes("someResponse");
bytes memory err = bytes("someErr");
uint32 totalGas = 5_000_000;
uint32 callbackGasLimit = 4_000_000;
address client = address(bigClient);

ZKSyncFunctionsRouter.CallbackResult memory result = _callback(
reqId,
resp,
err,
totalGas,
callbackGasLimit,
client
);
assertTrue(result.success, "Should succeed");
uint256 expectedMax = s_functionsRouter.MAX_CALLBACK_RETURN_BYTES();
// The returned data should be truncated exactly to expectedMax.
assertEq(result.returnData.length, expectedMax, "Should truncate data to MAX_CALLBACK_RETURN_BYTES");
}

/// @notice Internal helper to call the router's exposed callback function.
function _callback(
bytes32 reqId,
bytes memory resp,
bytes memory err,
uint32 totalGas,
uint32 callbackGasLimit,
address client
) internal returns (FunctionsRouter.CallbackResult memory) {
bytes memory payload = abi.encodeWithSelector(
s_functionsRouter.exposed_callback.selector,
reqId,
resp,
err,
callbackGasLimit,
client
);
(bool ok, bytes memory retData) = address(s_functionsRouter).call{gas: totalGas}(payload);
assertTrue(ok, "callback should succeed");
return abi.decode(retData, (FunctionsRouter.CallbackResult));
}
}

contract MockClientSuccess {
function handleOracleFulfillment(bytes32, bytes memory, bytes memory) external pure returns (bool) {
return true;
}
}

contract MockClientLargeReturn {
function handleOracleFulfillment(bytes32, bytes memory, bytes memory) external pure returns (bytes memory) {
// Return ~1,000 bytes.
bytes memory largeData = new bytes(1000);
for (uint i = 0; i < 1000; i++) {
largeData[i] = bytes1(uint8(65 + (i % 26))); // Fill with A..Z.
}
return largeData;
}
}

contract MockClientRevert {
function handleOracleFulfillment(bytes32, bytes memory, bytes memory) external pure returns (bool) {
revert("MockClientRevert");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {ZKSyncFunctionsRouter} from "../../../v1_3_0_zksync/ZKSyncFunctionsRouter.sol";

import {FunctionsRouter} from "../../../v1_0_0/FunctionsRouter.sol";

/// @title ZKSync Functions Router Test Harness
/// @notice Contract to expose internal functions for testing purposes
contract ZKSyncFunctionsRouterHarness is ZKSyncFunctionsRouter {
constructor(address linkToken, FunctionsRouter.Config memory config) ZKSyncFunctionsRouter(linkToken, config) {}

function exposed_callback(
bytes32 requestId,
bytes memory response,
bytes memory err,
uint32 callbackGasLimit,
address client
) public returns (CallbackResult memory) {
// simply call the internal `_callback` method
return super._callback(requestId, response, err, callbackGasLimit, client);
}
}
2 changes: 1 addition & 1 deletion contracts/src/v0.8/functions/v1_0_0/FunctionsRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ contract FunctionsRouter is IFunctionsRouter, FunctionsSubscriptions, Pausable,
bytes memory err,
uint32 callbackGasLimit,
address client
) private returns (CallbackResult memory) {
) internal virtual returns (CallbackResult memory) {
bool destinationNoLongerExists;
assembly {
// solidity calls check that a contract actually exists at the destination, so we do the same
Expand Down
Loading

0 comments on commit ff814eb

Please sign in to comment.