feat: add YieldRollup

This commit is contained in:
Andrea Franz
2025-06-04 12:15:18 +02:00
parent 6ca33e924d
commit bd90067a29
3 changed files with 261 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.30;
import { IMessageService } from "../../messaging/interfaces/IMessageService.sol";
import { L1MessageService } from "../../messaging/l1/L1MessageService.sol";
import { LineaRollupBase } from "../../rollup/LineaRollupBase.sol";
contract YieldRollup is LineaRollupBase {
error YieldRollup__L1ETHBridgeNotSet();
error YieldRollup__L2ETHBridgeNotSet();
error YieldRollup__InvalidValue();
error YieldRollup__InvalidRecipient();
address public l1ETHBridge;
address public l2ETHBridge;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(InitializationData calldata _initializationData) external initializer {
__LineaRollup_init(_initializationData);
}
/**
* @notice Sets the L1ETHBridge address.
* @param _l1ETHBridge The new L1ETHBridge address.
*/
function setL1ETHBridge(address _l1ETHBridge) public onlyRole(DEFAULT_ADMIN_ROLE) {
l1ETHBridge = _l1ETHBridge;
}
/**
* @notice Sets the L2ETHBridge address.
* @param _l2ETHBridge The new L2ETHBridge address.
*/
function setL2ETHBridge(address _l2ETHBridge) public onlyRole(DEFAULT_ADMIN_ROLE) {
l2ETHBridge = _l2ETHBridge;
}
/**
* @notice Sends a message. It doesn't allow sending ETH. If the message is sent to the L2ETHBridge, it checks if the sender is the L1ETHBridge.
* @param _to The recipient of the message.
* @param _fee The fee for the message.
* @param _calldata The calldata for the message.
*/
function sendMessage(
address _to,
uint256 _fee,
bytes calldata _calldata
) public payable override(IMessageService, L1MessageService) {
if (l1ETHBridge == address(0)) {
revert YieldRollup__L1ETHBridgeNotSet();
}
if (l2ETHBridge == address(0)) {
revert YieldRollup__L2ETHBridgeNotSet();
}
if (msg.value > 0) {
revert YieldRollup__InvalidValue();
}
if (_to == l2ETHBridge && msg.sender != l1ETHBridge) {
revert YieldRollup__InvalidRecipient();
}
_sendMessage(_to, _fee, _calldata);
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
library TestUtils {
// Helper function to convert address to ascii string
function _toAsciiString(address x) internal pure returns (string memory) {
bytes memory s = new bytes(42);
s[0] = "0";
s[1] = "x";
for (uint256 i = 0; i < 20; i++) {
uint8 b = uint8(uint256(uint160(x)) / (2 ** (8 * (19 - i))));
uint8 hi = b / 16;
uint8 lo = b - 16 * hi;
s[2 + 2 * i] = _char(hi);
s[3 + 2 * i] = _char(lo);
}
return string(s);
}
// Helper function to convert byte to char
function _char(uint8 b) internal pure returns (bytes1 c) {
if (b < 10) {
return bytes1(b + 0x30);
} else {
return bytes1(b + 0x57);
}
}
// Helper function to convert bytes32 to hex string
function _toHexString(bytes32 data) internal pure returns (string memory) {
return _toHexString(abi.encodePacked(data));
}
// Helper function to convert bytes to hex string
function _toHexString(bytes memory data) internal pure returns (string memory) {
bytes memory hexString = new bytes(data.length * 2 + 2);
hexString[0] = "0";
hexString[1] = "x";
bytes memory hexChars = "0123456789abcdef";
for (uint256 i = 0; i < data.length; i++) {
hexString[2 + i * 2] = hexChars[uint8(data[i] >> 4)];
hexString[3 + i * 2] = hexChars[uint8(data[i] & 0x0f)];
}
return string(hexString);
}
}

View File

@@ -0,0 +1,144 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import { Test } from "forge-std/Test.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { ILineaRollup } from "../../../../src/rollup/interfaces/ILineaRollup.sol";
import { IPermissionsManager } from "../../../../src/security/access/interfaces/IPermissionsManager.sol";
import { IPauseManager } from "../../../../src/security/pausing/interfaces/IPauseManager.sol";
import { IMessageService } from "../../../../src/messaging/interfaces/IMessageService.sol";
import { LineaRollup } from "../../../../src/rollup/LineaRollup.sol";
import { YieldRollup } from "../../../../src/yield/bridge/YieldRollup.sol";
import { TestUtils } from "./TestUtils.sol";
contract YieldRollupTest is Test {
YieldRollup yieldRollup;
address user1 = makeAddr("user1");
address l1ETHBridge = makeAddr("l1ETHBridge");
address l2ETHBridge = makeAddr("l2ETHBridge");
address operator = makeAddr("operator");
address defaultAdmin = makeAddr("defaultAdmin");
address verifier = makeAddr("verifier");
address nonAuthorizedAccount = makeAddr("nonAuthorizedAccount");
address securityCouncil = defaultAdmin;
address fallbackOperator = makeAddr("fallbackOperator");
bytes32 VERIFIER_SETTER_ROLE;
bytes32 VERIFIER_UNSETTER_ROLE;
bytes32 OPERATOR_ROLE;
bytes32 DEFAULT_ADMIN_ROLE;
function setUp() public {
YieldRollup implementation = new YieldRollup();
ILineaRollup.InitializationData memory initData;
initData.initialStateRootHash = bytes32(0x0);
initData.initialL2BlockNumber = 0;
initData.genesisTimestamp = block.timestamp;
initData.defaultVerifier = verifier;
initData.rateLimitPeriodInSeconds = 86400; // 1 day
initData.rateLimitAmountInWei = 100 ether;
initData.roleAddresses = new IPermissionsManager.RoleAddress[](1);
initData.roleAddresses[0] = IPermissionsManager.RoleAddress({
addressWithRole: operator,
role: implementation.OPERATOR_ROLE()
});
initData.pauseTypeRoles = new IPauseManager.PauseTypeRole[](0);
initData.unpauseTypeRoles = new IPauseManager.PauseTypeRole[](0);
initData.fallbackOperator = fallbackOperator;
initData.defaultAdmin = defaultAdmin;
bytes memory initializer = abi.encodeWithSelector(LineaRollup.initialize.selector, initData);
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initializer);
yieldRollup = YieldRollup(address(proxy));
VERIFIER_SETTER_ROLE = yieldRollup.VERIFIER_SETTER_ROLE();
VERIFIER_UNSETTER_ROLE = yieldRollup.VERIFIER_UNSETTER_ROLE();
OPERATOR_ROLE = yieldRollup.OPERATOR_ROLE();
DEFAULT_ADMIN_ROLE = yieldRollup.DEFAULT_ADMIN_ROLE();
assertEq(yieldRollup.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin), true, "Default admin not set");
assertEq(yieldRollup.hasRole(OPERATOR_ROLE, operator), true, "Operator not set");
vm.startBroadcast(defaultAdmin);
yieldRollup.setL1ETHBridge(l1ETHBridge);
yieldRollup.setL2ETHBridge(l2ETHBridge);
vm.stopBroadcast();
}
function test_RevertsIfL1ETHBridgeIsNotSet() public {
vm.prank(defaultAdmin);
yieldRollup.setL1ETHBridge(address(0));
vm.expectRevert("YieldRollup__L1ETHBridgeNotSet()");
yieldRollup.sendMessage(user1, 0, "");
}
function test_RevertsIfL2ETHBridgeIsNotSet() public {
vm.prank(defaultAdmin);
yieldRollup.setL2ETHBridge(address(0));
vm.expectRevert("YieldRollup__L2ETHBridgeNotSet()");
yieldRollup.sendMessage(user1, 0, "");
}
function test_RevertsIfInvalidValue() public {
vm.expectRevert("YieldRollup__InvalidValue()");
yieldRollup.sendMessage{ value: 1 }(user1, 0, "");
}
function test_RevertsIfInvalidRecipient() public {
vm.expectRevert("YieldRollup__InvalidRecipient()");
yieldRollup.sendMessage(l2ETHBridge, 0, "");
}
function test_OnlyAdminCanSetL1ETHBridge() public {
vm.prank(nonAuthorizedAccount);
vm.expectRevert(
abi.encodePacked(
"AccessControl: account ",
TestUtils._toAsciiString(nonAuthorizedAccount),
" is missing role ",
TestUtils._toHexString(DEFAULT_ADMIN_ROLE)
)
);
yieldRollup.setL1ETHBridge(l1ETHBridge);
}
function test_OnlyAdminCanSetL2ETHBridge() public {
vm.prank(nonAuthorizedAccount);
vm.expectRevert(
abi.encodePacked(
"AccessControl: account ",
TestUtils._toAsciiString(nonAuthorizedAccount),
" is missing role ",
TestUtils._toHexString(DEFAULT_ADMIN_ROLE)
)
);
yieldRollup.setL2ETHBridge(l2ETHBridge);
}
function test_SendsMessage() public {
vm.prank(l1ETHBridge);
vm.expectEmit();
emit IMessageService.MessageSent(
l1ETHBridge,
l2ETHBridge,
0,
0,
1,
"test-message",
keccak256(abi.encode(l1ETHBridge, l2ETHBridge, 0, 0, 1, "test-message"))
);
yieldRollup.sendMessage(l2ETHBridge, 0, "test-message");
// Verify message was sent
assertEq(yieldRollup.nextMessageNumber(), 2); // First message has nonce 1, so next should be 2
}
}