This repo routes contract calls through user-owned 6551 accounts to deposit into Eigenlayer from L2.
Eigenlayer does not allow ThirdParty withdrawals, users must use their wallets to deposit and withdraw funds. So we cannot withdraw on behalf of our users via L1 bridge contracts. Using 6551 proxies addresses this restriction.
This also keeps custody of funds with the user (who owns the 6551 NFT) and gives them an escape option to withdraw funds on L1.
- Running Tests and Restaking Scripts
- Sepolia L2 Restaking Example
- ERC-6551 EigenAgents
- Messaging and Bridging Behavior
- Message Encoding and Decoder
- Todo Features
The following test will bridge from L2 to L1, deposit in Eigenlayer, queueWithdrawals, completeWithdrawal, then bridge back to the original user on L2.
forge test --match-test test_FullFlow_CompleteWithdrawal -vvvv
The following test will test delegating, undelegating, and re-depositing:
forge test --match-test test_FullFlow_Undelegate_Delegate_Redeposit -vvvv
See test coverage
forge coverage
You can generate a lcov.info
file to see line-by-line test coverage in code editors, run this with:
forge coverage --report lcov
Frontend clients will make contract calls similar to the scripts in the scripts
folder. These scripts run on Base Sepolia and dispatches CCIP calls to Eth Sepolia, bridging CCIP-BnM
ERC20 tokens and interacting with mock Eigenlayer Strategy Vaults setup for the CCIP-BnM
token.
To run the Scripts see the scripts
folder:
5_depositAndMintEigenAgent.sh
: makes a cross-chain deposit into Eigenlayer from L2, minting an EigenAgent if the user does not yet have one.7_queueWithdrawal.sh
: queues a withdrawal from L2.8_completeWithdrawal.sh
: completes the withdrawal and bridges the deposit back from L1 into the original staker's wallet on L2.9_submitRewards.sh
: sets up token emission rewards for the week, however can only be done once per epoch (weekly)9b_processClaimRewards.sh
: claims token emissions rewards for the user and bridges tokens back to L2 user.
Scripts 2_deploySenderOnL2.s.sol
, 3_deployReceiverOnL1.s.sol
and 4_whitelistCCIPContracts.sh
deploy the CCIP bridge contracts, and 6551 and Eigenlayer Restaking handler contracts.
There are 2b
and 3b
upgrade scripts which need to be run when changes made to either the SenderCCIP
, ReceiverCCIP
,RestakingConnector
, SenderUtils
, AgentFactory
or EigenAgentOwner721
contracts.
The following scripts will test delegation:
6_delegateTo.sh
delegates to an Operator6b_undelegate.sh
undelegates from an Operator and begins a cooldown period (7 days on mainnet)6c_redeposit.sh
re-deposits shares into Eigenlayer and allows the user to re-delegate to another Operator. Everytime a user undelegates, there is a cooldown timer.
Bridging times depend on the finality times of source and destination chains. It currently takes +20 minutes to bridge a message Base and ZkSync has finality times of 20min, ETH is 15min
We first bridge 0.0619
tokens from L2 to L1 with a message to mint an ERC-6551 EigenAgent and forward a DepositIntoStrategy
message to Eigenlayer, resulting in Deposit
and 6551 EigenAgent minting events.
We can see tokens routing through the 6551 EigenAgent contract here.
Cost: 0.014686 ETH on Sepolia at 20.27 GWEI (724,221 gas)
approx. $38
Assuming 10 GWEI on mainnet: 0.00724 ETH
Assuming 30 GWEI on mainnet: 0.0217 ETH
See Tenderly transaction for an execution trace.
Users can send their EigenAgent a QueueWithdrawal message to withdraw, producing WithdrawalQueued events on L1.
After waiting for the unstaking period (7 days), users can complete withdrawals.
- Queued withdrawals information are stored in
script/withdrawals-queued/<user_address>/
- Completed withdrawals information are stored in
script/withdrawals-completed/<user_address>/
.
Sending a CompleteWithdrawal message from L2 executes on L1 with the following Eigenlayer WithdrawalCompleted events.
The L2 Bridge contract will automatically bridge the withdrawal back to L2 to the EigenAgent's owner. You can see the messageId
here in topic[1]
of the MessageSent event which we can track in the CCIP explorer to see the withdrawal bridging back from L1 to L2.
When the funds arrive on L2, the original 0.0619
tokens are transferred to the EigenAgent owner's address.
Note: As EigenAgentOwner NFTs are transferrable, a user may try call completeWithdrawal
then attempt to sell the NFT while the withdrawal is in flight. If users are trading these NFTs they need to be careful about this.
You can also claim staking rewards by sending a processClaim message from L2. The rewards are bridged back to L2 to the EigenAgent owner. Only the bridge token will be bridged back to L2. Other ERC20 reward tokens will be sent to the EigenAgent owner's wallet on L1.
You can delegate to Operators, by sending a delegateTo message, resulting in the following delegation events.
If a user wants to switch Operators to delegate to, they can send a undelegate message which results in the following undelegate events.
Undelegating queues the staker for withdrawal and produces withdrawalRoots
.
Front-end clients should keep track of the withdrawal information and withdrawalRoots
as they will be needed to re-deposit later.
After undelegating, users wait 7 days then delegate to another Operator. Then can re-deposit back into Eigenlayer with a redeposit Message, which results in the following re-deposit (WithdrawalCompleted) events.
Re-depositing uses the same function calls as completeWithdrawals
.
The receiveAsTokens
flag in completeWithdrawals
call determines whether user will:
- Receive withdrawals as tokens (which are bridged back to L2), or
- Receive withdrawals as shares in the vault (which can be re-delegated).
There is no way to directly re-delegate to another operator, a staker must undelegate + withdraw, wait 7 days, then restake and re-delegate to a new operator.
EigenAgent accounts will only execute calls if the signature came from the user who owns the associated EigenAgentOwner 721 NFT. See: https://eips.ethereum.org/EIPS/eip-6551
Each user can only have 1 EigenAgentOwner NFT at the moment. We can make them tradeable or soulbound.
EigenAgent accounts are ERC1967 Proxies and can be upgraded. We can also look at BeaconProxy implementation if we want upgradeability for all accounts (Agents just route contract calls, so upgradeability is not strictly needed).
EigenAgentOwner NFTs are minted via the AgentFactory (which talks to a 6551 Registry and keeps track of EigenAgent 6551 accounts and ownership).
Cost of deploying EigenAgent should be manageable (can swap for ERC1167 minimal proxies):
forge test --match-test test_step5c_MintEigenAgent -vvvv --gas-report
Note: at the moment you cannot have more than 1 cross-chain message in-flight at a time because the execution nonce will be stale, causing the EigenAgent execution signature to fail for the 2nd message.
- CCIP bridging takes ~20min (Ethereum finality takes ~12.8 min)
- A solution is to track in-flight txs and increment nonces on the client-side for subsequent messages (at least until the messages successfully execute on L1). Note this assumes CCIP messages land on L1 in the correct order.
When sending a TX to SenderCCIP bridge, if the CCIP message:
Contains Message | Sends Funds | Outcome |
---|---|---|
true | false | The TX reverts early on L2 if the Message is not a depositIntoStrategy function call. This makes it harder for frontend clients to make mistakes, e.g. frontend clients accidentally sending funds to L1 for a queueWithdrawal call. |
true | false | Tries to match an Eigenlayer function selector and execute that function (queueWithdrawal, claim rewards, delegate, etc). If no function selectors match, nothing happens. |
true | false | Simply bridges funds |
These choices are made because this repo is intended just for Eigenlayer function calls, not general purpose function calls.
Every message sent from L2 to L1 abi.encodes the message to send to Eigenlayer, then appends a user signature that signs the message digest of that Eigenlayer message to the end of it.
Please see the signMessageForEigenAgentExecution
function here.
An example of an Eigenlayer message would be the deposit message:
function encodeDepositIntoStrategyMsg(
address strategy,
address token,
uint256 amount
) public pure returns (bytes memory) {
return abi.encodeWithSelector(
IStrategyManager.depositIntoStrategy.selector,
strategy,
token,
amount
);
}
See it being used in a script here.
These messageWithSignatures
arrive on L1 and are decoded in the RestakingConnector.sol
contract.
In general, they follow the following format:
0000000000000000000000000000000000000000000000000000000000000020 [32] string offset CCIP
00000000000000000000000000000000000000000000000000000000000005a5 [64] string length CCIP
3ccc861d [96] Eigenlayer function selector
0000000000000000000000000000000000000000000000000000000000000000 [100] Eigenlayer calldata
0000000000000000000000008454d149beb26e3e3fc5ed1c87fb0b2a1b7b6c2c [132] ...
0000000000000000000000000000000000000000000000000000000000000054 [164] ...
0000000000000000000000000000000000000000000000000000000000010252 [196] ...
0000000000000000000000008454d149beb26e3e3fc5ed1c87fb0b2a1b7b6c2c [-124] signer
0000000000000000000000000000000000000000000000000000000000015195 [-92] expiry
03814b471f1beef18326b0d63c4a0f4431fdb72be167ee8aeb6212c8bd14d8e5 [-60] signature r
74fa9f4f34373bef152fdcba912a10b0a5c77be53c00d04c4c6c77ae407136e7 [-28] signature s
1b000000000000000000000000000000000000000000000000000000 [-0] signature v
The signature starts 124 bytes from the end of the message.
The decoding functions can be found in src/utils/EigenlayerMsgDecoders.sol
with correspoding tests in
test/UnitTests_MsgEncodingDecoding.t.sol
.
-
Cross-chain messages for EigenAgent to execute Eigenlayer actions:
-
depositIntoStrategy
- Catches deposit reverts, and allow manual re-execution to trigger refund after expiry (in case target Operator goes offline while deposits are in-flight from L2).
-
queueWithdrawals
-
completeQueuedWithdrawals
- Transfer withdrawn tokens back to L2.
- Transfer L1 tokens to AgentOwner address on L1.
-
delegateTo
-
undelegate
-
re-deposit
into Eigenlayer (and re-delegate). -
processClaim
staking rewards, and bridge rewards back to owner on L2.- Transfer bridgeable rewards tokens back to L2.
- Transfer L1 rewards tokens to AgentOwner address on L1.
-
-
Gas optimization
- Estimate gas limit for each of the previous operations
- Reduce gas costs associated with 6551 accounts creation + delegate calls
- Remove proxies if we don't need upgradeability.
-
Chainlink to setup a "lane" for CCIP bridges:
- Setup Chainlink lanes on Holesky and target L2.
- Adapt differences in bridging model (mint/burn vs lock/mint) for target chain.