Skip to content

Commit

Permalink
feat: L1 implementation of Lido integration
Browse files Browse the repository at this point in the history
  • Loading branch information
swarna1101 committed Jun 20, 2024
1 parent 04d8c87 commit ac4763e
Show file tree
Hide file tree
Showing 11 changed files with 557 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/protocol/contracts/thirdparty/lido/.gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
6 changes: 6 additions & 0 deletions packages/protocol/contracts/thirdparty/lido/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions packages/protocol/contracts/thirdparty/lido/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@openzeppelin=lib/openzeppelin-contracts/contracts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2022 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.20;

/// @author psirex
/// @notice Contains the logic for validation of tokens used in the bridging process
contract BridgeableTokens {
/// @notice Address of the bridged token in the L1 chain
address public immutable l1Token;

/// @notice Address of the token minted on the L2 chain when token bridged
address public immutable l2Token;

/// @param l1Token_ Address of the bridged token in the L1 chain
/// @param l2Token_ Address of the token minted on the L2 chain when token bridged
constructor(address l1Token_, address l2Token_) {
l1Token = l1Token_;
l2Token = l2Token_;
}

/// @dev Validates that passed l1Token_ is supported by the bridge
modifier onlySupportedL1Token(address l1Token_) {
if (l1Token_ != l1Token) {
revert ErrorUnsupportedL1Token();
}
_;
}

/// @dev Validates that passed l2Token_ is supported by the bridge
modifier onlySupportedL2Token(address l2Token_) {
if (l2Token_ != l2Token) {
revert ErrorUnsupportedL2Token();
}
_;
}

/// @dev validates that account_ is not zero address
modifier onlyNonZeroAccount(address account_) {
if (account_ == address(0)) {
revert ErrorAccountIsZeroAddress();
}
_;
}

error ErrorUnsupportedL1Token();
error ErrorUnsupportedL2Token();
error ErrorAccountIsZeroAddress();
}
130 changes: 130 additions & 0 deletions packages/protocol/contracts/thirdparty/lido/src/BridgingManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2022 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.20;

import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";

/// @author psirex
/// @notice Contains administrative methods to retrieve and control the state of the bridging
contract BridgingManager is AccessControl {
/// @dev Stores the state of the bridging
/// @param isInitialized Shows whether the contract is initialized or not
/// @param isDepositsEnabled Stores the state of the deposits
/// @param isWithdrawalsEnabled Stores the state of the withdrawals
struct State {
bool isInitialized;
bool isDepositsEnabled;
bool isWithdrawalsEnabled;
}

bytes32 public constant DEPOSITS_ENABLER_ROLE =
keccak256("BridgingManager.DEPOSITS_ENABLER_ROLE");
bytes32 public constant DEPOSITS_DISABLER_ROLE =
keccak256("BridgingManager.DEPOSITS_DISABLER_ROLE");
bytes32 public constant WITHDRAWALS_ENABLER_ROLE =
keccak256("BridgingManager.WITHDRAWALS_ENABLER_ROLE");
bytes32 public constant WITHDRAWALS_DISABLER_ROLE =
keccak256("BridgingManager.WITHDRAWALS_DISABLER_ROLE");

/// @dev The location of the slot with State
bytes32 private constant STATE_SLOT = keccak256("BridgingManager.bridgingState");

/// @notice Initializes the contract to grant DEFAULT_ADMIN_ROLE to the admin_ address
/// @dev This method might be called only once
/// @param admin_ Address of the account to grant the DEFAULT_ADMIN_ROLE
function initialize(address admin_) external {
State storage s = _loadState();
if (s.isInitialized) {
revert ErrorAlreadyInitialized();
}
_grantRole(DEFAULT_ADMIN_ROLE, admin_);
s.isInitialized = true;
emit Initialized(admin_);
}

/// @notice Returns whether the contract is initialized or not
function isInitialized() public view returns (bool) {
return _loadState().isInitialized;
}

/// @notice Returns whether the deposits are enabled or not
function isDepositsEnabled() public view returns (bool) {
return _loadState().isDepositsEnabled;
}

/// @notice Returns whether the withdrawals are enabled or not
function isWithdrawalsEnabled() public view returns (bool) {
return _loadState().isWithdrawalsEnabled;
}

/// @notice Enables the deposits if they are disabled
function enableDeposits() external onlyRole(DEPOSITS_ENABLER_ROLE) {
if (isDepositsEnabled()) {
revert ErrorDepositsEnabled();
}
_loadState().isDepositsEnabled = true;
emit DepositsEnabled(msg.sender);
}

/// @notice Disables the deposits if they aren't disabled yet
function disableDeposits() external whenDepositsEnabled onlyRole(DEPOSITS_DISABLER_ROLE) {
_loadState().isDepositsEnabled = false;
emit DepositsDisabled(msg.sender);
}

/// @notice Enables the withdrawals if they are disabled
function enableWithdrawals() external onlyRole(WITHDRAWALS_ENABLER_ROLE) {
if (isWithdrawalsEnabled()) {
revert ErrorWithdrawalsEnabled();
}
_loadState().isWithdrawalsEnabled = true;
emit WithdrawalsEnabled(msg.sender);
}

/// @notice Disables the withdrawals if they aren't disabled yet
function disableWithdrawals()
external
whenWithdrawalsEnabled
onlyRole(WITHDRAWALS_DISABLER_ROLE)
{
_loadState().isWithdrawalsEnabled = false;
emit WithdrawalsDisabled(msg.sender);
}

/// @dev Returns the reference to the slot with State struct
function _loadState() private pure returns (State storage r) {
bytes32 slot = STATE_SLOT;
assembly {
r.slot := slot
}
}

/// @dev Validates that deposits are enabled
modifier whenDepositsEnabled() {
if (!isDepositsEnabled()) {
revert ErrorDepositsDisabled();
}
_;
}

/// @dev Validates that withdrawals are enabled
modifier whenWithdrawalsEnabled() {
if (!isWithdrawalsEnabled()) {
revert ErrorWithdrawalsDisabled();
}
_;
}

event DepositsEnabled(address indexed enabler);
event DepositsDisabled(address indexed disabler);
event WithdrawalsEnabled(address indexed enabler);
event WithdrawalsDisabled(address indexed disabler);
event Initialized(address indexed admin);

error ErrorDepositsEnabled();
error ErrorDepositsDisabled();
error ErrorWithdrawalsEnabled();
error ErrorWithdrawalsDisabled();
error ErrorAlreadyInitialized();
}
41 changes: 41 additions & 0 deletions packages/protocol/contracts/thirdparty/lido/src/L1Executor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.20;

import { IL1Executor } from "./interfaces/IL1Executor.sol";

/// @dev Helper contract for contracts performing cross-domain communications
contract L1Executor {
/// @notice Messenger contract used to send and receive messages from the other domain
IL1Executor public immutable messenger;

/// @param messenger_ Address of the CrossDomainMessenger on the current layer
constructor(address messenger_) {
messenger = IL1Executor(messenger_);
}

/// @dev Sends a message to an account on another domain
/// @param crossDomainTarget_ Intended recipient on the destination domain
/// @param message_ Data to send to the target (usually calldata to a function with
/// `onlyFromCrossDomainAccount()`)
/// @param gasLimit_ gasLimit for the receipt of the message on the target domain.
function sendMessage(
address crossDomainTarget_,
uint32 gasLimit_,
bytes memory message_
)
internal
{
messenger.sendMessage(crossDomainTarget_, message_, gasLimit_);
}

/// @dev Enforces that the modified function is only callable by a specific cross-domain account
modifier onlyFromCrossDomainAccount() {
if (msg.sender != address(messenger)) {
revert ErrorUnauthorizedMessenger();
}
_;
}

error ErrorUnauthorizedMessenger();
}
110 changes: 110 additions & 0 deletions packages/protocol/contracts/thirdparty/lido/src/L1TokenBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.20;

// Library Imports
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

// Local imports
import { L1Executor } from "./L1Executor.sol";
import { BridgingManager } from "./BridgingManager.sol";
import { BridgeableTokens } from "./BridgeableTokens.sol";
import { IL1TokenBridge } from "./interfaces/IL1TokenBridge.sol";
import { IL2TokenBridge } from "./interfaces/IL2TokenBridge.sol";

/// @notice The L1 Standard bridge locks bridged tokens on the L1 side, sends deposit messages
/// on the L2 side, and finalizes token withdrawals from L2.
contract L1TokenBridge is IL1TokenBridge, BridgeableTokens, BridgingManager, L1Executor {
using SafeERC20 for IERC20;

address public immutable l2TokenBridge;

constructor(
address l1Token_,
address l2Token_,
address messenger_, // L1 messenger address being used for cross-chain communications
address l2TokenBridge_
)
BridgeableTokens(l1Token_, l2Token_)
L1Executor(messenger_)
{
l2TokenBridge = l2TokenBridge_;
}

function depositTo(
address l1Token_,
address to_,
uint256 amount_,
uint32 l2Gas_,
bytes calldata data_
)
public
whenDepositsEnabled
onlySupportedL1Token(l1Token_)
onlyNonZeroAccount(to_)
{
_initiateTokenDeposit(msg.sender, to_, amount_, l2Gas_, data_);
}

function deposit(
address l1Token_,
uint256 amount_,
uint32 l2Gas_,
bytes calldata data_
)
external
{
depositTo(l1Token_, msg.sender, amount_, l2Gas_, data_);
}

function finalizeWithdrawal(
address l1Token_,
address l2Token_,
address from_,
address to_,
uint256 amount_,
bytes calldata data_
)
external
whenWithdrawalsEnabled
onlyFromCrossDomainAccount
onlySupportedL1Token(l1Token_)
onlySupportedL2Token(l2Token_)
{
uint256 before_balance = IERC20(l1Token).balanceOf(address(this));
IERC20(l1Token).safeTransfer(to_, amount_); // Transfer to User
uint256 after_balance = IERC20(l1Token).balanceOf(address(this));

// To handle Fee-on-Transafer and other misc tokens
require(after_balance - before_balance == amount_, "Incorrect Funds Transferred");

emit TokenWithdrawalFinalized(l1Token_, l2Token_, from_, to_, amount_, data_);
}

function _initiateTokenDeposit(
address from_,
address to_,
uint256 amount_,
uint32 l2Gas_,
bytes calldata data_
)
internal
{
uint256 before_balance = IERC20(l1Token).balanceOf(address(this));
IERC20(l1Token).safeTransferFrom(from_, address(this), amount_); // Transfer From user.
uint256 after_balance = IERC20(l1Token).balanceOf(address(this));

// To handle Fee-on-Transafer and other misc tokens
require(after_balance - before_balance == amount_, "Incorrect Funds Transferred");

bytes memory message = abi.encodeWithSelector(
IL2TokenBridge.finalizeDeposit.selector, l1Token, l2Token, from_, to_, amount_, data_
);
// Sends Cross Domain Message
sendMessage(l2TokenBridge, l2Gas_, message);

emit TokenDepositInitiated(l1Token, l2Token, from_, to_, amount_, data_);
}
}
Loading

0 comments on commit ac4763e

Please sign in to comment.