mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-09 04:08:01 -05:00
feature(L1ETHBridge): add L1ETHBridge that wraps a message for the L2ETHBridge contracts and forwards the funds to the yield manager
This commit is contained in:
41
contracts/scripts/yield/bridge/Base.s.sol
Normal file
41
contracts/scripts/yield/bridge/Base.s.sol
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { Script } from "forge-std/Script.sol";
|
||||
|
||||
abstract contract BaseScript is Script {
|
||||
/// @dev Included to enable compilation of the script without a $MNEMONIC environment variable.
|
||||
string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk";
|
||||
|
||||
/// @dev Needed for the deterministic deployments.
|
||||
bytes32 internal constant ZERO_SALT = bytes32(0);
|
||||
|
||||
/// @dev The address of the transaction broadcaster.
|
||||
address internal broadcaster;
|
||||
|
||||
/// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined.
|
||||
string internal mnemonic;
|
||||
|
||||
/// @dev Initializes the transaction broadcaster like this:
|
||||
///
|
||||
/// - If $ETH_FROM is defined, use it.
|
||||
/// - Otherwise, derive the broadcaster address from $MNEMONIC.
|
||||
/// - If $MNEMONIC is not defined, default to a test mnemonic.
|
||||
///
|
||||
/// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line.
|
||||
constructor() {
|
||||
address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) });
|
||||
if (from != address(0)) {
|
||||
broadcaster = from;
|
||||
} else {
|
||||
mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC });
|
||||
(broadcaster, ) = deriveRememberKey({ mnemonic: mnemonic, index: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
modifier broadcast() {
|
||||
vm.startBroadcast(broadcaster);
|
||||
_;
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
}
|
||||
28
contracts/scripts/yield/bridge/DeployL1ETHBridge.s.sol
Normal file
28
contracts/scripts/yield/bridge/DeployL1ETHBridge.s.sol
Normal file
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { BaseScript } from "./Base.s.sol";
|
||||
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
|
||||
import { DeploymentConfig } from "./DeploymentConfig.s.sol";
|
||||
import { L1ETHBridge } from "../../../src/yield/bridge/L1ETHBridge.sol";
|
||||
|
||||
contract DeployL1ETHBridge is BaseScript {
|
||||
function run() public returns (address, L1ETHBridge) {
|
||||
DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster);
|
||||
(address deployer, address rollup, address yieldManager, address l2ETHBridge) = deploymentConfig
|
||||
.activeNetworkConfig();
|
||||
|
||||
vm.startBroadcast(deployer);
|
||||
|
||||
L1ETHBridge impl = new L1ETHBridge();
|
||||
bytes memory initializeData = abi.encodeCall(L1ETHBridge.initialize, (deployer, rollup, yieldManager, l2ETHBridge));
|
||||
|
||||
address proxy = address(new ERC1967Proxy(address(impl), initializeData));
|
||||
|
||||
vm.stopBroadcast();
|
||||
|
||||
return (deployer, L1ETHBridge(proxy));
|
||||
}
|
||||
}
|
||||
|
||||
52
contracts/scripts/yield/bridge/DeploymentConfig.s.sol
Normal file
52
contracts/scripts/yield/bridge/DeploymentConfig.s.sol
Normal file
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { Script } from "forge-std/Script.sol";
|
||||
import { ETHYieldManagerMock } from "../../../test/foundry/yield/bridge/mocks/ETHYieldManagerMock.sol";
|
||||
import { RollupMock } from "../../../test/foundry/yield/bridge/mocks/RollupMock.sol";
|
||||
|
||||
contract DeploymentConfig is Script {
|
||||
error DeploymentConfig_InvalidDeployerAddress();
|
||||
error DeploymentConfig_NoConfigForChain(uint256);
|
||||
|
||||
struct NetworkConfig {
|
||||
address deployer;
|
||||
address rollup;
|
||||
address yieldManager;
|
||||
address l2ETHBridge;
|
||||
}
|
||||
|
||||
NetworkConfig public activeNetworkConfig;
|
||||
|
||||
address private deployer;
|
||||
|
||||
constructor(address _broadcaster) {
|
||||
if (_broadcaster == address(0)) revert DeploymentConfig_InvalidDeployerAddress();
|
||||
deployer = _broadcaster;
|
||||
if (block.chainid == 31_337) {
|
||||
activeNetworkConfig = getOrCreateAnvilEthConfig(deployer);
|
||||
} else {
|
||||
revert DeploymentConfig_NoConfigForChain(block.chainid);
|
||||
}
|
||||
}
|
||||
|
||||
function getOrCreateAnvilEthConfig(address _deployer) public returns (NetworkConfig memory) {
|
||||
ETHYieldManagerMock yieldManager = new ETHYieldManagerMock();
|
||||
RollupMock rollup = new RollupMock();
|
||||
|
||||
return
|
||||
NetworkConfig({
|
||||
deployer: _deployer,
|
||||
rollup: address(rollup),
|
||||
yieldManager: address(yieldManager),
|
||||
l2ETHBridge: makeAddr("l2ETHBridge")
|
||||
});
|
||||
}
|
||||
|
||||
// This function is a hack to have it excluded by `forge coverage` until
|
||||
// https://github.com/foundry-rs/foundry/issues/2988 is fixed.
|
||||
// See: https://github.com/foundry-rs/foundry/issues/2988#issuecomment-1437784542
|
||||
// for more info.
|
||||
// solhint-disable-next-line
|
||||
function test() public {}
|
||||
}
|
||||
119
contracts/src/yield/bridge/L1ETHBridge.sol
Normal file
119
contracts/src/yield/bridge/L1ETHBridge.sol
Normal file
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.19;
|
||||
|
||||
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
||||
|
||||
import { IRollup } from "./interfaces/IRollup.sol";
|
||||
import { IL2ETHBridge } from "./interfaces/IL2ETHBridge.sol";
|
||||
|
||||
contract L1ETHBridge is Initializable, UUPSUpgradeable, OwnableUpgradeable {
|
||||
error L1ETHBridge__ZeroValue();
|
||||
error L1ETHBridge__RollupAddressNotSet();
|
||||
error L1ETHBridge__YieldManagerNotSet();
|
||||
error L1ETHBridge__YieldManagerDepositFailed();
|
||||
error L1ETHBridge__L2ETHBridgeNotSet();
|
||||
|
||||
address public rollup;
|
||||
address public yieldManager;
|
||||
address public l2ETHBridge;
|
||||
|
||||
modifier rollupIsSet() {
|
||||
if (rollup == address(0)) {
|
||||
revert L1ETHBridge__RollupAddressNotSet();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier yieldManagerIsSet() {
|
||||
if (yieldManager == address(0)) {
|
||||
revert L1ETHBridge__YieldManagerNotSet();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier l2ETHBridgeIsSet() {
|
||||
if (l2ETHBridge == address(0)) {
|
||||
revert L1ETHBridge__L2ETHBridgeNotSet();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes the contract.
|
||||
* @dev Disables initializers to prevent reinitialization.
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes the contract.
|
||||
* @param _initialOwner The initial owner of the contract.
|
||||
* @param _rollup The rollup address.
|
||||
* @param _yieldManager The yield manager address.
|
||||
* @param _l2ETHBridge The L2ETHBridge address.
|
||||
*/
|
||||
function initialize(
|
||||
address _initialOwner,
|
||||
address _rollup,
|
||||
address _yieldManager,
|
||||
address _l2ETHBridge
|
||||
) external initializer {
|
||||
_transferOwnership(_initialOwner);
|
||||
rollup = _rollup;
|
||||
yieldManager = _yieldManager;
|
||||
l2ETHBridge = _l2ETHBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the rollup address.
|
||||
* @param _rollup The new rollup address.
|
||||
*/
|
||||
function setRollup(address _rollup) external onlyOwner {
|
||||
rollup = _rollup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the yield manager address.
|
||||
* @param _yieldManager The new yield manager address.
|
||||
*/
|
||||
function setYieldManager(address _yieldManager) external onlyOwner {
|
||||
yieldManager = _yieldManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the L2ETHBridge address.
|
||||
* @param _l2ETHBridge The new L2ETHBridge address.
|
||||
*/
|
||||
function setL2ETHBridge(address _l2ETHBridge) external onlyOwner {
|
||||
l2ETHBridge = _l2ETHBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Bridges ETH to the L2ETHBridge.
|
||||
* @param _to The recipient address on the L2.
|
||||
* @param _calldata The calldata to be sent to the L2ETHBridge.
|
||||
*/
|
||||
function bridgeETH(
|
||||
address _to,
|
||||
bytes memory _calldata
|
||||
) external payable rollupIsSet yieldManagerIsSet l2ETHBridgeIsSet {
|
||||
if (msg.value == 0) {
|
||||
revert L1ETHBridge__ZeroValue();
|
||||
}
|
||||
|
||||
(bool success, ) = yieldManager.call{ value: msg.value }("");
|
||||
if (!success) {
|
||||
revert L1ETHBridge__YieldManagerDepositFailed();
|
||||
}
|
||||
|
||||
bytes memory data = abi.encodeWithSelector(IL2ETHBridge.completeBridge.selector, _to, msg.value, _calldata);
|
||||
IRollup(rollup).sendMessage(l2ETHBridge, 0, data);
|
||||
}
|
||||
|
||||
function _authorizeUpgrade(address) internal view override {
|
||||
_checkOwner();
|
||||
}
|
||||
}
|
||||
6
contracts/src/yield/bridge/interfaces/IL2ETHBridge.sol
Normal file
6
contracts/src/yield/bridge/interfaces/IL2ETHBridge.sol
Normal file
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.19;
|
||||
|
||||
interface IL2ETHBridge {
|
||||
function completeBridge(address _to, uint256 _value, bytes calldata _calldata) external;
|
||||
}
|
||||
7
contracts/src/yield/bridge/interfaces/IRollup.sol
Normal file
7
contracts/src/yield/bridge/interfaces/IRollup.sol
Normal file
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.19;
|
||||
|
||||
interface IRollup {
|
||||
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable;
|
||||
}
|
||||
|
||||
93
contracts/test/foundry/yield/bridge/L1ETHBridge.t.sol
Normal file
93
contracts/test/foundry/yield/bridge/L1ETHBridge.t.sol
Normal file
@@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { Test } from "forge-std/Test.sol";
|
||||
import { L1ETHBridge } from "../../../../src/yield/bridge/L1ETHBridge.sol";
|
||||
import { DeployL1ETHBridge } from "../../../../scripts/yield/bridge/DeployL1ETHBridge.s.sol";
|
||||
import { IL2ETHBridge } from "../../../../src/yield/bridge/interfaces/IL2ETHBridge.sol";
|
||||
|
||||
import { RollupMock } from "./mocks/RollupMock.sol";
|
||||
import { ETHYieldManagerMock } from "./mocks/ETHYieldManagerMock.sol";
|
||||
|
||||
contract L1ETHBridgeTest is Test {
|
||||
L1ETHBridge bridge;
|
||||
RollupMock rollup;
|
||||
ETHYieldManagerMock yieldManager;
|
||||
|
||||
address deployer;
|
||||
address l2ETHBridge;
|
||||
address user1 = makeAddr("user1");
|
||||
address user2 = makeAddr("user2");
|
||||
|
||||
function setUp() public {
|
||||
DeployL1ETHBridge script = new DeployL1ETHBridge();
|
||||
(deployer, bridge) = script.run();
|
||||
rollup = RollupMock(bridge.rollup());
|
||||
yieldManager = ETHYieldManagerMock(payable(bridge.yieldManager()));
|
||||
l2ETHBridge = bridge.l2ETHBridge();
|
||||
}
|
||||
}
|
||||
|
||||
contract BridgeETHTest is L1ETHBridgeTest {
|
||||
function test_RevertsIfYieldManagerIsNotSet() public {
|
||||
vm.prank(deployer);
|
||||
bridge.setYieldManager(address(0));
|
||||
|
||||
vm.expectRevert("L1ETHBridge__YieldManagerNotSet()");
|
||||
bridge.bridgeETH(address(0), "");
|
||||
}
|
||||
|
||||
function test_RevertsIfRollupIsNotSet() public {
|
||||
vm.prank(deployer);
|
||||
bridge.setRollup(address(0));
|
||||
|
||||
vm.expectRevert("L1ETHBridge__RollupAddressNotSet()");
|
||||
bridge.bridgeETH(address(0), "");
|
||||
}
|
||||
|
||||
function test_RevertsIfL2ETHBridgeIsNotSet() public {
|
||||
vm.prank(deployer);
|
||||
bridge.setL2ETHBridge(address(0));
|
||||
|
||||
vm.expectRevert("L1ETHBridge__L2ETHBridgeNotSet()");
|
||||
bridge.bridgeETH(address(0), "");
|
||||
}
|
||||
|
||||
function test_RevertsIfValueIsZero() public {
|
||||
vm.expectRevert("L1ETHBridge__ZeroValue()");
|
||||
bridge.bridgeETH(address(0), "");
|
||||
}
|
||||
|
||||
function test_FundsAreForwardedToYieldManager() public {
|
||||
vm.deal(user1, 100);
|
||||
vm.prank(user1);
|
||||
bridge.bridgeETH{ value: 100 }(user2, "");
|
||||
|
||||
assertEq(yieldManager.depositsLength(), 1);
|
||||
ETHYieldManagerMock.Deposit memory deposit = yieldManager.lastDeposit();
|
||||
assertEq(deposit.from, address(bridge));
|
||||
assertEq(deposit.value, 100);
|
||||
assertEq(address(yieldManager).balance, 100);
|
||||
}
|
||||
|
||||
function test_MessagesAreSentToL2ETHBridge() public {
|
||||
vm.deal(user1, 100);
|
||||
vm.prank(user1);
|
||||
bridge.bridgeETH{ value: 100 }(user2, "test-message");
|
||||
|
||||
assertEq(rollup.messagesLength(), 1);
|
||||
|
||||
bytes memory expectedData = abi.encodeWithSelector(
|
||||
IL2ETHBridge.completeBridge.selector,
|
||||
user2,
|
||||
100,
|
||||
"test-message"
|
||||
);
|
||||
|
||||
RollupMock.Message memory message = rollup.lastMessage();
|
||||
assertEq(message.to, l2ETHBridge);
|
||||
assertEq(message.fee, 0);
|
||||
assertEq(message.value, 0);
|
||||
assertEq(message.data, expectedData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
contract ETHYieldManagerMock {
|
||||
struct Deposit {
|
||||
address from;
|
||||
uint256 value;
|
||||
}
|
||||
|
||||
Deposit[] public deposits;
|
||||
|
||||
receive() external payable {
|
||||
deposits.push(Deposit({ from: msg.sender, value: msg.value }));
|
||||
}
|
||||
|
||||
function depositsLength() external view returns (uint256) {
|
||||
return deposits.length;
|
||||
}
|
||||
|
||||
function lastDeposit() external view returns (Deposit memory) {
|
||||
require(deposits.length > 0, "No deposits made");
|
||||
return deposits[deposits.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
29
contracts/test/foundry/yield/bridge/mocks/RollupMock.sol
Normal file
29
contracts/test/foundry/yield/bridge/mocks/RollupMock.sol
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { IRollup } from "../../../../../src/yield/bridge/interfaces/IRollup.sol";
|
||||
|
||||
contract RollupMock is IRollup {
|
||||
struct Message {
|
||||
address to;
|
||||
uint256 fee;
|
||||
uint256 value;
|
||||
bytes data;
|
||||
}
|
||||
|
||||
Message[] public messages;
|
||||
|
||||
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable {
|
||||
messages.push(Message({ to: _to, fee: _fee, value: msg.value, data: _calldata }));
|
||||
}
|
||||
|
||||
function messagesLength() external view returns (uint256) {
|
||||
return messages.length;
|
||||
}
|
||||
|
||||
function lastMessage() external view returns (Message memory) {
|
||||
require(messages.length > 0, "No messages made");
|
||||
return messages[messages.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user