Skip to content

Commit

Permalink
Merge pull request #8 from stader-labs/feat/arb-native-staking
Browse files Browse the repository at this point in the history
 ETHx pool for arbitrum native staking
  • Loading branch information
blockgroot authored Jul 26, 2024
2 parents 6c255c4 + 8000195 commit 5230676
Show file tree
Hide file tree
Showing 24 changed files with 5,762 additions and 3 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ $ forge test -v
| ETHx ProxyAdmin | 0xAAE054B9b822554dd1D9d1F48f892B4585D3bbf0 | Arbitrum |
| ETHx | 0xED65C5085a18Fa160Af0313E60dcc7905E944Dc7 | Arbitrum |
| ETHx_OFT | 0x6904603c27392310D19E389105CA792FB935C43C | Arbitrum |
| ETHxPoolV1 | 0x107E427848937C4217C0efc1C3f35091de08E45e | Arbitrum |
| ETHxPoolV1 ProxyAdmin | 0xe9609DcbC830784b3b68D5b4162c5193Df5dfA98 | Arbitrum |
| ETHx ProxyAdmin | 0x8bc3646d175ECb081469Be6a0b2A10eeE112101C | Optimism |
| ETHx | 0xc54B43eaF921A5194c7973A4d65E055E5a1453c2 | Optimism |
| ETHx_OFT | 0x01aF04690d17DC27b891A7F67E9EEe4d14DE8EA8 | Optimism |
Expand All @@ -91,6 +93,8 @@ $ forge test -v
| ETHx | 0x52312ea29135A468417F0C71d6A75CfEA75351b7 | Arbitrum Sepolia |
| ETHx_OFT | 0x8AEDA11bD0C5fBafbFb142830d23812Df02A8424 | Arbitrum Sepolia |
| ETHxRateReceiver | 0x2b700f8b3F03798e7Db0e67a5aB48c12D10046DE | Arbitrum Sepolia |
| ETHxPoolV1 | 0xA6457927857107c1B6EBe82fe0d17E78ba03cb6A | Arbitrum Sepolia |
| ETHxPoolV1 ProxyAdmin | 0xe7a85781f16403C9AF078C81e603F8465870df7B | Arbitrum Sepolia |
| ETHx ProxyAdmin | 0xb30256CA8A9Ebe058Eb78a4edbf3364e7F8e5d86 | XLayer Testnet |
| ETHx | 0x7D03Bfa72Cd70e96A391cF32e7B27e43AE68a574 | XLayer Testnet |
| ETHx_OFT | 0xD99E8bA5259Dd2b8B9aBFE0eD78913ec60B8F898 | XLayer Testnet |
Expand Down
186 changes: 186 additions & 0 deletions contracts/L2/ETHxPoolV1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.22;

import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @title ETHxPool Contract
* @author Stader Labs
* @notice This contract is responsible for the swap of ETHx; the user must deposit ETH in order to receive ETHx in
* return.
*/
interface AggregatorV3Interface {
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}

contract ETHxPoolV1 is AccessControlUpgradeable, ReentrancyGuardUpgradeable {
using SafeERC20 for IERC20;

/// @notice Role hash of MANAGER
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
/// @notice Conversion factor from ether to wei
uint256 public constant ETHER_TO_WEI = 1e18;
/// @notice Address of ETHx token
IERC20 public ETHx;
/// @notice Basis points for fees
uint256 public feeBps;
/// @notice Total fees earned in swap
uint256 public feeEarnedInETH;
/// @notice Address of the ETHx/ETH oracle
address public ethxOracle;

/// @notice Emitted when swap occured successfully
event SwapOccurred(address indexed user, uint256 ETHxAmount, uint256 fee, string referralId);
/// @notice Emitted when accumulated fees is withdrawn
event FeesWithdrawn(uint256 feeEarnedInETH);
/// @notice Emitted when deposited ETH is withdrawn
event WithdrawCollectedETH(uint256 ethBalanceMinusFees);
/// @notice Emitted when provisioned ETHx is withdrawn
event WithdrawETHx(uint256 amount);
/// @notice Emitted when basis fee is updated
event FeeBpsSet(uint256 feeBps);
/// @notice Emitted when oracle address is updated
event OracleSet(address indexed oracle);

/// @dev Thrown when input is zero address
error ZeroAddress();
/// @dev Thrown when input is invalid amount
error InvalidAmount();
/// @dev Thrown when input is invalid basis fee
error InvalidBps();
/// @dev Thrown when input fee is too high
error HighFees();
/// @dev Thrown when transfer is failed
error TransferFailed();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @dev Initialize the contract
/// @param _admin The admin address
/// @param _manager The manager address
/// @param _ethx The ETHx token address
/// @param _feeBps The fee basis points
/// @param _ethxOracle The ethxOracle address
function initialize(
address _admin,
address _manager,
address _ethx,
uint256 _feeBps,
address _ethxOracle
)
public
initializer
{
_checkNonZeroAddress(_ethx);
_checkNonZeroAddress(_ethxOracle);

__AccessControl_init();
__ReentrancyGuard_init();

_grantRole(DEFAULT_ADMIN_ROLE, _admin);
_setupRole(MANAGER_ROLE, _manager);
_setupRole(MANAGER_ROLE, _admin);

ETHx = IERC20(_ethx);
feeBps = _feeBps;
ethxOracle = _ethxOracle;
}

/// @dev Swaps ETH for ETHx
/// @param _referralId The referral id
function swapETHToETHx(string memory _referralId) external payable nonReentrant {
uint256 amount = msg.value;

if (amount == 0) revert InvalidAmount();

(uint256 ethxAmount, uint256 fee) = viewSwapETHxAmountAndFee(amount);

feeEarnedInETH += fee;

ETHx.safeTransfer(msg.sender, ethxAmount);

emit SwapOccurred(msg.sender, ethxAmount, fee, _referralId);
}

/// @dev view function to get the ETHx amount for a given amount of ETH
/// @param _amount The amount of ETH
/// @return ethxAmount The amount of ETHx that will be received
/// @return fee The fee that will be charged
function viewSwapETHxAmountAndFee(uint256 _amount) public view returns (uint256 ethxAmount, uint256 fee) {
fee = _amount * feeBps / 10_000;
uint256 amountAfterFee = _amount - fee;

(, int256 ethxToEthRate,,,) = AggregatorV3Interface(ethxOracle).latestRoundData();

// Calculate the final ETHx amount
ethxAmount = amountAfterFee * ETHER_TO_WEI / uint256(ethxToEthRate);
}

/*//////////////////////////////////////////////////////////////
ACCESS RESTRICTED FUNCTIONS
//////////////////////////////////////////////////////////////*/

/// @dev Withdraws fees earned by the pool
function withdrawFees(address _receiver) external onlyRole(MANAGER_ROLE) {
// withdraw fees in ETH
uint256 amountToSendInETH = feeEarnedInETH;
feeEarnedInETH = 0;
(bool success,) = payable(_receiver).call{ value: amountToSendInETH }("");
if (!success) revert TransferFailed();

emit FeesWithdrawn(amountToSendInETH);
}

/// @dev Withdraws collected ETH from the contract
function withdrawCollectedETH() external onlyRole(MANAGER_ROLE) {
// withdraw ETH - fees
uint256 ethBalanceMinusFees = address(this).balance - feeEarnedInETH;

(bool success,) = msg.sender.call{ value: ethBalanceMinusFees }("");
if (!success) revert TransferFailed();

emit WithdrawCollectedETH(ethBalanceMinusFees);
}

/// @dev Withdraws provisioned ETHx
function withdrawETHx(uint256 amount) external onlyRole(MANAGER_ROLE) {
if (amount > ETHx.balanceOf(address(this))) revert InvalidAmount();

ETHx.safeTransfer(msg.sender, amount);

emit WithdrawETHx(amount);
}

/// @dev Sets the fee basis points
/// @param _feeBps The fee basis points
function setFeeBps(uint256 _feeBps) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (_feeBps > 10_000) revert InvalidBps();
if (_feeBps > 1000) revert HighFees();

feeBps = _feeBps;

emit FeeBpsSet(_feeBps);
}

/// @dev Sets the ethxOracle address
/// @param _ethxOracle The ethxOracle address
function setETHXOracle(address _ethxOracle) external onlyRole(DEFAULT_ADMIN_ROLE) {
_checkNonZeroAddress(_ethxOracle);
ethxOracle = _ethxOracle;
emit OracleSet(_ethxOracle);
}

function _checkNonZeroAddress(address _addr) private pure {
if (_addr == address(0)) {
revert ZeroAddress();
}
}
}
63 changes: 63 additions & 0 deletions deploy/ethxPoolV1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'assert'
import { type DeployFunction } from 'hardhat-deploy/types'

const feeBps = 10;

const networkAddresses = {
arbitrumsepolia: {
owner: "0xfcB068B43AB08aA9210F52eabd261A7a3b0C8357",
manager: "0xfcB068B43AB08aA9210F52eabd261A7a3b0C8357",
ethx: "0x52312ea29135A468417F0C71d6A75CfEA75351b7",
ethxOracle: "0x2b700f8b3F03798e7Db0e67a5aB48c12D10046DE"
},
arbitrumone: {
owner: "0xe85F0d083D0CD18485E531c1A8B8a05ad2C0308f",
manager: "0xc6160F5bC3C673AC390f11c492E8ED0d0693579A",
ethx: "0xED65C5085a18Fa160Af0313E60dcc7905E944Dc7",
ethxOracle: "0xB4AC4078DDA43d0eB6Bb9e08b8C12A73f9FEAA7d"
}
}

const deployETHxPool: DeployFunction = async (hre) => {
const { getNamedAccounts, deployments } = hre
const { deploy } = deployments
const { deployer } = await getNamedAccounts()

assert(deployer, 'Missing named deployer account')

console.log(`Network: ${hre.network.name}`)
console.log(`Deployer: ${deployer}`)

const networkName = hre.network.name.toLowerCase()
const networkConfig = networkAddresses[networkName]
console.log(networkConfig);


assert(networkConfig, `No network configuration found for ${networkName}`)

const ethxPool = await deploy("ETHxPoolV1", {
from: deployer,
contract: "ETHxPoolV1",
proxy: {
owner: networkConfig.owner,
proxyContract: "OpenZeppelinTransparentProxy",
execute: {
methodName: "initialize",
args: [
networkConfig.owner,
networkConfig.manager,
networkConfig.ethx,
feeBps,
networkConfig.ethxOracle
],
},
},
autoMine: true,
log: true,
});

console.log(`Deployed contract: ETHxPoolV1, network: ${hre.network.name}, address: ${ethxPool.address}`)
}

deployETHxPool.tags = ["ETHxPoolV1"]
export default deployETHxPool
1 change: 1 addition & 0 deletions deployments/arbitrumone/.chainId
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
42161
Loading

0 comments on commit 5230676

Please sign in to comment.