Skip to content

[Chore] POC for auto send on received #904

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

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions contracts/src/_testing/unit/NoFundReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0

pragma solidity 0.8.30;

contract NoFundReceiver {
fallback() external payable {
// forced empty revert for code coverage
revert();
}

receive() external payable {
revert();
}
}
20 changes: 20 additions & 0 deletions contracts/src/messaging/l1/L1MessageService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ abstract contract L1MessageService is
uint256 _fee,
bytes calldata _calldata
) external payable whenTypeAndGeneralNotPaused(PauseType.L1_L2) {
_sendMessage(_to, _fee, _calldata);
}

/**
* @notice Adds a message for sending cross-chain and emits MessageSent.
* @dev The message number is preset (nextMessageNumber) and only incremented at the end if successful for the next caller.
* @dev This function should be called with a msg.value = _value + _fee. The fee will be paid on the destination chain.
* @param _to The address the message is intended for.
* @param _fee The fee being paid for the message delivery.
* @param _calldata The calldata to pass to the recipient.
*/
function _sendMessage(address _to, uint256 _fee, bytes memory _calldata) internal {
if (_to == address(0)) {
revert ZeroAddressNotAllowed();
}
Expand Down Expand Up @@ -147,4 +159,12 @@ abstract contract L1MessageService is
function sender() external view returns (address originalSender) {
originalSender = TRANSIENT_MESSAGE_SENDER;
}

/**
* @notice Receives ETH and does an automatic no calldata ETH send message to L2.
* @dev The to and from addresses will always be the msg.sender.
*/
receive() external payable {
_sendMessage(msg.sender, 0, "");
}
}
22 changes: 21 additions & 1 deletion contracts/src/messaging/l2/v1/L2MessageServiceV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,20 @@ abstract contract L2MessageServiceV1 is
* @param _fee The fee being paid for the message delivery.
* @param _calldata The calldata to pass to the recipient.
*/
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable {
function sendMessage(address _to, uint256 _fee, bytes memory _calldata) external payable {
_requireTypeAndGeneralNotPaused(PauseType.L2_L1);

_sendMessage(_to, _fee, _calldata);
}

/**
* @notice Adds a message for sending cross-chain and emits a relevant event.
* @dev The message number is preset and only incremented at the end if successful for the next caller.
* @param _to The address the message is intended for.
* @param _fee The fee being paid for the message delivery.
* @param _calldata The calldata to pass to the recipient.
*/
function _sendMessage(address _to, uint256 _fee, bytes memory _calldata) internal {
if (_to == address(0)) {
revert ZeroAddressNotAllowed();
}
Expand Down Expand Up @@ -213,4 +224,13 @@ abstract contract L2MessageServiceV1 is
}
}
}

/**
* @notice Receives ETH and does an automatic no calldata ETH send message to L2.
* @dev The to and from addresses will always be the msg.sender.
* @dev The minimumFeeInWei is set as it is a prerequisite for L2->L1 messaging.
*/
receive() external payable {
_sendMessage(msg.sender, minimumFeeInWei, "");
}
}
22 changes: 17 additions & 5 deletions contracts/src/messaging/libraries/MessageHashing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ library MessageHashing {
* @notice Hashes messages using assembly for efficiency.
* @dev Adding 0xc0 is to indicate the calldata offset relative to the memory being added to.
* @dev If the calldata is not modulus 32, the extra bit needs to be added on at the end else the hash is wrong.
* @dev mcopy cannot be used due to limitations on L2. This will be modified in the future.
* @param _from The from address.
* @param _to The to address.
* @param _fee The fee paid for delivery.
Expand All @@ -24,7 +25,7 @@ library MessageHashing {
uint256 _fee,
uint256 _valueSent,
uint256 _messageNumber,
bytes calldata _calldata
bytes memory _calldata
) internal pure returns (bytes32 messageHash) {
assembly {
let mPtr := mload(0x40)
Expand All @@ -34,15 +35,26 @@ library MessageHashing {
mstore(add(mPtr, 0x60), _valueSent)
mstore(add(mPtr, 0x80), _messageNumber)
mstore(add(mPtr, 0xa0), 0xc0)
mstore(add(mPtr, 0xc0), _calldata.length)
let rem := mod(_calldata.length, 0x20)
let dataLen := mload(_calldata)
mstore(add(mPtr, 0xc0), dataLen)

let dataPtr := add(_calldata, 0x20)
let destPtr := add(mPtr, 0xe0)
for {
let i := 0
} lt(i, dataLen) {
i := add(i, 0x20)
} {
mstore(add(destPtr, i), mload(add(dataPtr, i)))
}

let rem := mod(dataLen, 0x20)
let extra := 0
if iszero(iszero(rem)) {
extra := sub(0x20, rem)
}

calldatacopy(add(mPtr, 0xe0), _calldata.offset, _calldata.length)
messageHash := keccak256(mPtr, add(0xe0, add(_calldata.length, extra)))
messageHash := keccak256(mPtr, add(0xe0, add(dataLen, extra)))
}
}
}
30 changes: 28 additions & 2 deletions contracts/test/hardhat/L1MessageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,32 @@ describe("L1MessageService", () => {
});
});

describe("Receive test", () => {
it("Should send a message when receive is invoked", async () => {
const adminAddress = await admin.getAddress();

const expectedBytes = await encodeSendMessage(
adminAddress,
adminAddress,
0n,
MESSAGE_VALUE_1ETH,
1n,
EMPTY_CALLDATA,
);

const messageHash = ethers.keccak256(expectedBytes);

const eventArgs = [adminAddress, adminAddress, 0n, MESSAGE_VALUE_1ETH, 1, EMPTY_CALLDATA, messageHash];

await expectEvent(
l1MessageService,
admin.sendTransaction({ to: await l1MessageService.getAddress(), value: MESSAGE_VALUE_1ETH }),
"MessageSent",
eventArgs,
);
});
});

describe("Send messages", () => {
it("Should fail when the fee is higher than the amount sent", async () => {
const sendMessageCall = l1MessageService
Expand Down Expand Up @@ -892,7 +918,7 @@ describe("L1MessageService", () => {
MESSAGE_FEE,
MESSAGE_VALUE_1ETH,
1n,
EMPTY_CALLDATA,
"0x1234",
);

await l1MessageService.addL2L1MessageHash(ethers.keccak256(expectedBytes));
Expand All @@ -905,7 +931,7 @@ describe("L1MessageService", () => {
MESSAGE_FEE,
MESSAGE_VALUE_1ETH,
ADDRESS_ZERO,
EMPTY_CALLDATA,
"0x1234",
1,
),
)
Expand Down
58 changes: 27 additions & 31 deletions contracts/test/hardhat/messaging/l2/L2MessageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
import { TestL2MessageService, TestReceivingContract } from "../../../../typechain-types";
import { NoFundReceiver, TestL2MessageService, TestReceivingContract } from "../../../../typechain-types";
import {
ADDRESS_ZERO,
BLOCK_COINBASE,
Expand Down Expand Up @@ -47,6 +47,8 @@ import {

describe("L2MessageService", () => {
let l2MessageService: TestL2MessageService;
let noFundReceiver: NoFundReceiver;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
let admin: SignerWithAddress;
let securityCouncil: SignerWithAddress;
Expand All @@ -66,6 +68,10 @@ describe("L2MessageService", () => {
]) as unknown as Promise<TestL2MessageService>;
}

async function deployNoFundReceiverFixture() {
return deployUpgradableFromFactory("NoFundReceiver") as unknown as Promise<NoFundReceiver>;
}

before(async () => {
[admin, securityCouncil, l1l2MessageSetter, notAuthorizedAccount, postmanAddress] = await ethers.getSigners();
roleAddresses = generateRoleAssignments(L2_MESSAGE_SERVICE_ROLES, securityCouncil.address, [
Expand All @@ -75,6 +81,7 @@ describe("L2MessageService", () => {

beforeEach(async () => {
l2MessageService = await loadFixture(deployL2MessageServiceFixture);
noFundReceiver = await loadFixture(deployNoFundReceiverFixture);
});

describe("Initialization checks", () => {
Expand Down Expand Up @@ -216,7 +223,7 @@ describe("L2MessageService", () => {
it("Should fail when the coinbase fee transfer fails", async () => {
await l2MessageService.connect(securityCouncil).setMinimumFee(MINIMUM_FEE);

await ethers.provider.send("hardhat_setCoinbase", [await l2MessageService.getAddress()]);
await ethers.provider.send("hardhat_setCoinbase", [await noFundReceiver.getAddress()]);

const sendMessageCall = l2MessageService
.connect(admin)
Expand All @@ -225,7 +232,7 @@ describe("L2MessageService", () => {
});

await expectRevertWithCustomError(l2MessageService, sendMessageCall, "FeePaymentFailed", [
await l2MessageService.getAddress(),
await noFundReceiver.getAddress(),
]);

await ethers.provider.send("hardhat_setCoinbase", [BLOCK_COINBASE]);
Expand Down Expand Up @@ -1163,42 +1170,31 @@ describe("L2MessageService", () => {
await expectRevertWithReason(claimMessageCall, "ReentrancyGuard: reentrant call");
});

it("Should fail when the destination errors through receive", async () => {
it("Should send a message when receive is invoked", async () => {
const adminAddress = await admin.getAddress();
const setMinimumFeeInWei = await l2MessageService.minimumFeeInWei();

const expectedBytes = await encodeSendMessage(
await l2MessageService.getAddress(),
await l2MessageService.getAddress(),
MESSAGE_FEE,
adminAddress,
adminAddress,
0n,
MESSAGE_VALUE_1ETH,
1n,
EMPTY_CALLDATA,
);

await l2MessageService.addFunds({ value: INITIAL_WITHDRAW_LIMIT });

const expectedBytesArray = [ethers.keccak256(expectedBytes)];
const expectedRollingHash = calculateRollingHash(ethers.ZeroHash, ethers.keccak256(expectedBytes));

await l2MessageService.setLastAnchoredL1MessageNumber(1);
await l2MessageService
.connect(l1l2MessageSetter)
.anchorL1L2MessageHashes(expectedBytesArray, 2, 2, expectedRollingHash);
const messageHash = ethers.keccak256(expectedBytes);

await expect(
l2MessageService
.connect(admin)
.claimMessage(
await l2MessageService.getAddress(),
await l2MessageService.getAddress(),
MESSAGE_FEE,
MESSAGE_VALUE_1ETH,
ADDRESS_ZERO,
EMPTY_CALLDATA,
1,
),
).to.be.reverted;
const eventArgs = [adminAddress, adminAddress, 0n, MESSAGE_VALUE_1ETH, 1, EMPTY_CALLDATA, messageHash];

expect(await l2MessageService.inboxL1L2MessageStatus(ethers.keccak256(expectedBytes))).to.be.equal(
INBOX_STATUS_RECEIVED,
await expectEvent(
l2MessageService,
admin.sendTransaction({
to: await l2MessageService.getAddress(),
value: MESSAGE_VALUE_1ETH + setMinimumFeeInWei,
}),
"MessageSent",
eventArgs,
);
});

Expand Down
8 changes: 2 additions & 6 deletions contracts/test/hardhat/rollup/LineaRollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,12 @@ describe("Linea Rollup contract", () => {
({ verifier, lineaRollup } = await loadFixture(deployLineaRollupFixture));
});

describe("Fallback/Receive tests", () => {
describe("Fallback tests", () => {
const sendEthToContract = async (data: string) => {
return admin.sendTransaction({ to: await lineaRollup.getAddress(), value: INITIAL_WITHDRAW_LIMIT, data });
};

it("Should fail to send eth to the lineaRollup contract through the fallback", async () => {
await expect(sendEthToContract(EMPTY_CALLDATA)).to.be.reverted;
});

it("Should fail to send eth to the lineaRollup contract through the receive function", async () => {
it("Should fail to send eth to the lineaRollup contract through the fallback function", async () => {
await expect(sendEthToContract("0x1234")).to.be.reverted;
});
});
Expand Down
Loading