Skip to content

[OETHb] Direct Staking #2293

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
184 changes: 184 additions & 0 deletions contracts/contracts/crosschain/AbstractDirectStakingHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { CCIPReceiver } from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import { IARM } from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IARM.sol";

import { IRouterClient } from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";

import { ICCIPRouter } from "../interfaces/chainlink/ICCIPRouter.sol";
import { Governable } from "../governance/Governable.sol";

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

abstract contract AbstractDirectStakingHandler is CCIPReceiver, Governable {
using SafeERC20 for IERC20;

struct ChainConfig {
bool isSupported;
// Address of the Handler on the chain
address handlerAddr;
}

mapping(uint64 => ChainConfig) public chainConfigs;

// For future use
uint256[49] private __gap;

event ChainConfigAdded(uint64 chainId, address handlerAddr);
event ChainConfigRemoved(uint64 chainId);

/**
* @dev Reverts if CCIP's Risk Management contract (ARM) is cursed
*/
modifier onlyIfNotCursed() {
IARM arm = IARM(ICCIPRouter(this.getRouter()).getArmProxy());

Check warning on line 37 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L36-L37

Added lines #L36 - L37 were not covered by tests

require(!arm.isCursed(), "CCIP Router is cursed");

_;

Check warning on line 41 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L41

Added line #L41 was not covered by tests
}

constructor(address _router) CCIPReceiver(_router) {
// Nobody owns the implementation
_setGovernor(address(0));
}

/**
* @dev Adds a chain config
*/
function addChainConfig(uint64 chainId, address _handler)

Check warning on line 52 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L52

Added line #L52 was not covered by tests
external
onlyGovernor
{
require(!chainConfigs[chainId].isSupported, "Chain config exists");

emit ChainConfigAdded(chainId, _handler);

Check warning on line 58 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L58

Added line #L58 was not covered by tests

chainConfigs[chainId] = ChainConfig({

Check warning on line 60 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L60

Added line #L60 was not covered by tests
isSupported: true,
handlerAddr: _handler
});
}

/**
* @dev Removes a chain config
*/
function removeChainConfig(uint64 chainId) external onlyGovernor {
require(chainConfigs[chainId].isSupported, "Unknown chain ID");

emit ChainConfigRemoved(chainId);

Check warning on line 72 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L72

Added line #L72 was not covered by tests

delete chainConfigs[chainId];

Check warning on line 74 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L74

Added line #L74 was not covered by tests
}

/**
* @dev Returns the CCIP fees that the contract needs to execute the command
* @param chainSelector Destination chain
* @param data Message data
* @param tokenAddr Token to send
* @param tokenAmount Amount of tokens to send
* @param maxGasLimit Max Gas Limit to use
*/
function getCCIPFees(

Check warning on line 85 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L85

Added line #L85 was not covered by tests
uint64 chainSelector,
bytes memory data,
address tokenAddr,
uint256 tokenAmount,
uint256 maxGasLimit
) external view returns (uint256) {
// Build the message
(, uint256 fee) = _buildCCIPMessage(

Check warning on line 93 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L93

Added line #L93 was not covered by tests
chainSelector,
data,
tokenAddr,
tokenAmount,
maxGasLimit
);

return fee;

Check warning on line 101 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L101

Added line #L101 was not covered by tests
}

/**
* @dev Builds the CCIP message
* @param chainSelector Destination chain
* @param data Message data
* @param tokenAddr Token to send
* @param tokenAmount Amount of tokens to send
* @param maxGasLimit Max Gas Limit to use
*/
function _buildCCIPMessage(

Check warning on line 112 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L112

Added line #L112 was not covered by tests
uint64 chainSelector,
bytes memory data,
address tokenAddr,
uint256 tokenAmount,
uint256 maxGasLimit
)
internal
view
returns (Client.EVM2AnyMessage memory message, uint256 fee)
{
ChainConfig memory config = chainConfigs[chainSelector];

Check warning on line 123 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L123

Added line #L123 was not covered by tests

require(config.isSupported, "Unsupported destination chain");

bytes memory extraArgs = hex"";

Check warning on line 127 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L127

Added line #L127 was not covered by tests
// Set gas limit if needed
if (maxGasLimit > 0) {
extraArgs = Client._argsToBytes(

Check warning on line 130 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L130

Added line #L130 was not covered by tests
// Set gas limit
Client.EVMExtraArgsV1({ gasLimit: maxGasLimit })
);
}

// Tokens to transfer
Client.EVMTokenAmount[]

Check warning on line 137 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L137

Added line #L137 was not covered by tests
memory tokenAmounts = new Client.EVMTokenAmount[](1);
tokenAmounts[0] = Client.EVMTokenAmount({

Check warning on line 139 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L139

Added line #L139 was not covered by tests
token: tokenAddr,
amount: tokenAmount
});

// Build the message
message = Client.EVM2AnyMessage({

Check warning on line 145 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L145

Added line #L145 was not covered by tests
receiver: abi.encode(config.handlerAddr),
data: data,
tokenAmounts: tokenAmounts,
extraArgs: extraArgs,
feeToken: address(0)
});

// Estimate fee
fee = IRouterClient(i_router).getFee(chainSelector, message);

Check warning on line 154 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L154

Added line #L154 was not covered by tests
}

/**
* @dev Grant token approval for contracts that can move tokens from here
*/
function approveAllTokens() external virtual;

/**
* @notice Owner function to withdraw a specific amount of a token
* @param token token to be transferered
* @param amount amount of the token to be transferred
*/
function transferToken(address token, uint256 amount)

Check warning on line 167 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L167

Added line #L167 was not covered by tests
external
onlyGovernor
nonReentrant
{
if (token == address(0)) {
// Transfer out Native token to governor
(bool success, ) = _governor().call{ value: amount }("");

Check warning on line 174 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L174

Added line #L174 was not covered by tests
require(success, "Native token transfer failed");
return;

Check warning on line 176 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L176

Added line #L176 was not covered by tests
}

IERC20(token).safeTransfer(_governor(), amount);

Check warning on line 179 in contracts/contracts/crosschain/AbstractDirectStakingHandler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/AbstractDirectStakingHandler.sol#L179

Added line #L179 was not covered by tests
}

// Accept ETH to pay for gas
receive() external payable {}
}
7 changes: 7 additions & 0 deletions contracts/contracts/crosschain/CCIPChainSelector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Ref: https://docs.chain.link/ccip/supported-networks/v1_2_0/mainnet
uint64 constant MAINNET_SELECTOR = 5009297550715157269;
uint64 constant ARBITRUM_ONE_SELECTOR = 4949039107694359620;
uint64 constant BASE_SELECTOR = 15971525489660198786;
183 changes: 183 additions & 0 deletions contracts/contracts/crosschain/DirectStakingL2Handler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { CCIPReceiver } from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import { IRouterClient } from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";

import { IVault } from "../interfaces/IVault.sol";
import { IOUSD } from "../interfaces/IOUSD.sol";
import { IDirectStakingCaller } from "../interfaces/IDirectStakingCaller.sol";

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC4626 } from "../../lib/openzeppelin/interfaces/IERC4626.sol";

import { AbstractDirectStakingHandler } from "./AbstractDirectStakingHandler.sol";

import { MAINNET_SELECTOR } from "./CCIPChainSelector.sol";

contract DirectStakingL2Handler is AbstractDirectStakingHandler {
IERC20 public immutable weth;
IERC4626 public immutable woeth;

struct StakeRequest {
address requester;
bool processed;
bool callback;
uint256 amountIn;
uint256 minAmountOut;
uint256 amountReceived;
uint256 createdAt;
}
mapping(bytes32 => StakeRequest) public stakeRequests;

event DirectStakeRequestCreated(
bytes32 messageId,
uint64 destChain,
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut,
uint256 fee
);

event DirectStakeRequestCompleted(bytes32 messageId, uint256 amountOut);

constructor(
address _router,
address _weth,
address _woeth
) AbstractDirectStakingHandler(_router) {
weth = IERC20(_weth);
woeth = IERC4626(_woeth);
}

function _ccipReceive(Client.Any2EVMMessage memory message)

Check warning on line 55 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L55

Added line #L55 was not covered by tests
internal
virtual
override
onlyIfNotCursed
nonReentrant
{
// Make sure it's from mainnet
require(
message.sourceChainSelector == MAINNET_SELECTOR,
"Not from mainnet"
);

ChainConfig memory cc = chainConfigs[MAINNET_SELECTOR];

Check warning on line 68 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L68

Added line #L68 was not covered by tests

// Make sure mainnet is marked as supported
require(cc.isSupported, "Mainnet not configured");
require(
abi.decode(message.sender, (address)) == cc.handlerAddr,
"Unknown sender"
);

// Make sure it contains wOETH from mainnet
require(
message.destTokenAmounts.length == 1,
"Invalid tokens received"
);

Client.EVMTokenAmount memory tokenAmount = message.destTokenAmounts[0];

Check warning on line 83 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L83

Added line #L83 was not covered by tests
require(tokenAmount.token == address(woeth), "Unsupported token");
require(tokenAmount.amount > 0, "No tokens received");

// Decode messageId
bytes32 originalMessageId = abi.decode(message.data, (bytes32));

Check warning on line 88 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L88

Added line #L88 was not covered by tests

// Make sure the requests exists
StakeRequest memory request = stakeRequests[originalMessageId];

Check warning on line 91 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L91

Added line #L91 was not covered by tests
require(request.requester != address(0), "Unknown request message");

// And that it wasn't processed
require(!request.processed, "Already processed");

// Check slippage
require(tokenAmount.amount >= request.minAmountOut, "Slippage error");

// Mark as processed
stakeRequests[originalMessageId].processed = true;

Check warning on line 101 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L101

Added line #L101 was not covered by tests
// Store amount received (would be easier to lookup/debug if needed)
stakeRequests[originalMessageId].amountReceived = tokenAmount.amount;

Check warning on line 103 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L103

Added line #L103 was not covered by tests

emit DirectStakeRequestCompleted(originalMessageId, tokenAmount.amount);

Check warning on line 105 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L105

Added line #L105 was not covered by tests

// Transfer tokens to the caller
// slither-disable-start unused-return
// slither-disable-next-line unchecked-transfer
woeth.transfer(request.requester, tokenAmount.amount);

Check warning on line 110 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L110

Added line #L110 was not covered by tests
// slither-disable-end unused-return

if (request.callback) {
// If requester needs a callback, invoke it
IDirectStakingCaller(request.requester)

Check warning on line 115 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L115

Added line #L115 was not covered by tests
.onDirectStakingRequestCompletion(
originalMessageId,
tokenAmount.amount
);
}
}

function stake(

Check warning on line 123 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L123

Added line #L123 was not covered by tests
uint256 wethAmount,
uint256 minAmountOut,
bool callback
) external payable nonReentrant returns (bytes32) {
require(wethAmount > 0, "Invalid amount");

ChainConfig memory cc = chainConfigs[MAINNET_SELECTOR];

Check warning on line 130 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L130

Added line #L130 was not covered by tests

// Make sure mainnet is marked as supported
require(cc.isSupported, "Mainnet not configured");

// Transfer WETH in
// slither-disable-start unused-return
// slither-disable-next-line unchecked-transfer
weth.transferFrom(msg.sender, address(this), wethAmount);

Check warning on line 138 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L138

Added line #L138 was not covered by tests
// slither-disable-end unused-return

// Build message to initiate
(Client.EVM2AnyMessage memory message, uint256 fee) = _buildCCIPMessage(

Check warning on line 142 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L142

Added line #L142 was not covered by tests
MAINNET_SELECTOR,
abi.encode(minAmountOut),
address(weth),
wethAmount,
// Just a rough gas estimation
700000
);

bytes32 messageId = IRouterClient(i_router).ccipSend{ value: fee }(

Check warning on line 151 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L151

Added line #L151 was not covered by tests
MAINNET_SELECTOR,
message
);

emit DirectStakeRequestCreated(

Check warning on line 156 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L156

Added line #L156 was not covered by tests
messageId,
MAINNET_SELECTOR,
address(weth),
address(woeth),
wethAmount,
minAmountOut,
fee
);

stakeRequests[messageId] = StakeRequest({

Check warning on line 166 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L166

Added line #L166 was not covered by tests
requester: msg.sender,
processed: false,
callback: callback,
amountIn: wethAmount,
minAmountOut: minAmountOut,
amountReceived: 0,
createdAt: block.timestamp
});

return messageId;

Check warning on line 176 in contracts/contracts/crosschain/DirectStakingL2Handler.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/crosschain/DirectStakingL2Handler.sol#L176

Added line #L176 was not covered by tests
}

function approveAllTokens() external override onlyGovernor {
// Allow CCIP Router to move WETH from here
weth.approve(i_router, type(uint256).max);
}
}
Loading
Loading