Skip to content
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

chore: add token contracts #535

Merged
merged 13 commits into from
May 7, 2024
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/openzeppelin-contracts-v4.9.0"]
path = lib/openzeppelin-contracts-v4.9.0
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/openzeppelin-contracts-upgradeable-v4.9.0"]
path = lib/openzeppelin-contracts-upgradeable-v4.9.0
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts-v4.9.0
4 changes: 3 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@openzeppelin-upgrades/=lib/openzeppelin-contracts-upgradeable/
@openzeppelin/=lib/openzeppelin-contracts/
@openzeppelin-v4.9.0/=lib/openzeppelin-contracts-v4.9.0/
@openzeppelin-upgrades-v4.9.0/=lib/openzeppelin-contracts-upgradeable-v4.9.0/
ds-test/=lib/ds-test/src/
forge-std/=lib/forge-std/src/
forge-std/=lib/forge-std/src/
40 changes: 40 additions & 0 deletions src/contracts/interfaces/IBackingEigen.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;

import "@openzeppelin-v4.9.0/contracts/token/ERC20/IERC20.sol";

interface IBackingEigen is IERC20 {

/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
*/
function setAllowedFrom(address from, bool isAllowedFrom) external;

/**
* @notice This function allows the owner to set the allowedTo status of an address
* @param to the address whose allowedTo status is being set
* @param isAllowedTo the new allowedTo status
*/
function setAllowedTo(address to, bool isAllowedTo) external;

/**
* @notice Allows the owner to disable transfer restrictions
*/
function disableTransferRestrictions() external;


/**
* @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based
* checkpoints (and voting).
*/
function clock() external view returns (uint48);

/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
* Has been overridden to inform callers that this contract uses timestamps instead of block numbers, to match `clock()`
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() external pure returns (string memory);
}
54 changes: 54 additions & 0 deletions src/contracts/interfaces/IEigen.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.5.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
wadealexc marked this conversation as resolved.
Show resolved Hide resolved

interface IEigen is IERC20 {

/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
*/
function setAllowedFrom(address from, bool isAllowedFrom) external;

/**
* @notice This function allows the owner to set the allowedTo status of an address
* @param to the address whose allowedTo status is being set
* @param isAllowedTo the new allowedTo status
*/
function setAllowedTo(address to, bool isAllowedTo) external;

/**
* @notice Allows the owner to disable transfer restrictions
*/
function disableTransferRestrictions() external;

/**
* @notice This function allows minter to mint tokens
*/
function mint() external;

/**
* @notice This function allows bEIGEN holders to wrap their tokens into Eigen
*/
function wrap(uint256 amount) external;

/**
* @notice This function allows Eigen holders to unwrap their tokens into bEIGEN
*/
function unwrap(uint256 amount) external;

/**
* @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based
* checkpoints (and voting).
*/
function clock() external view returns (uint48);

/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
* Has been overridden to inform callers that this contract uses timestamps instead of block numbers, to match `clock()`
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() external pure returns (string memory);
}
105 changes: 105 additions & 0 deletions src/contracts/strategies/EigenStrategy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity =0.8.12;
gpsanant marked this conversation as resolved.
Show resolved Hide resolved

// NOTE: Mainnet uses the OpenZeppelin v4.9.0 contracts, but this imports the 4.7.1 version. This will be changed after an upgrade.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so is the plan to upgrade all contracts to 4.9.0 ?
it's definitely ugly to have both versions present at once

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could do it with the next major core release (PEPE/slashing). we should stay away from doing this during payments - dont want to touch core contracts unless we absolutely need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to figure out how to deal with the EIP712 storage change

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the issue was primarily when doing a downgrade, upgrades are designed to be possible. but yes, good call - will require some review

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../interfaces/IStrategyManager.sol";
import "../strategies/StrategyBase.sol";
import "../interfaces/IEigen.sol";

/**
* @title Eigen Strategy implementation of `IStrategy` interface, designed to be inherited from by more complex strategies.
* @author Layr Labs, Inc.
* @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service
* @dev Note that this EigenStrategy contract is designed to be compatible with both bEIGEN and EIGEN tokens. It functions exactly the same
* as the `StrategyBase` contract if bEIGEN were the underlying token, but also allows for depositing and withdrawing EIGEN tokens. This is
* achieved by unwrapping EIGEN into bEIGEN upon deposit, and wrapping bEIGEN into EIGEN upon withdrawal. Deposits and withdrawals with bEIGEN
* does not perform and wrapping or unwrapping.
* @notice This contract functions similarly to an ERC4626 vault, only without issuing a token.
* To mitigate against the common "inflation attack" vector, we have chosen to use the 'virtual shares' mitigation route,
* similar to [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol).
* We acknowledge that this mitigation has the known downside of the virtual shares causing some losses to users, which are pronounced
* particularly in the case of the share exchange rate changing signficantly, either positively or negatively.
* For a fairly thorough discussion of this issue and our chosen mitigation strategy, we recommend reading through
* [this thread](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706) on the OpenZeppelin repo.
* We specifically use a share offset of `SHARES_OFFSET` and a balance offset of `BALANCE_OFFSET`.
*/
contract EigenStrategy is StrategyBase {
using SafeERC20 for IERC20;

/**
* @notice EIGEN can be deposited into this strategy, where it is unwrapped into bEIGEN and staked in
* this strategy contract. EIGEN can also be withdrawn by withdrawing bEIGEN from this strategy, and
* then wrapping it back into EIGEN.
*/
IEigen public EIGEN;

/// @notice Since this contract is designed to be initializable, the constructor simply sets `strategyManager`, the only immutable variable.
constructor(IStrategyManager _strategyManager) StrategyBase(_strategyManager) {}

function initialize(
IEigen _EIGEN,
IERC20 _bEIGEN,
IPauserRegistry _pauserRegistry
) public virtual initializer {
EIGEN = _EIGEN;
_initializeStrategyBase(_bEIGEN, _pauserRegistry);
}

/**
* @notice This function hook is called in EigenStrategy.deposit() and is overridden here to
* allow for depositing of either EIGEN or bEIGEN tokens. If token is bEIGEN aka the underlyingToken,
* then the contract functions exactly the same as the StrategyBase contract and the deposit is calculated into shares.
* If token is EIGEN, then the EIGEN is first 1-1 unwrapped into bEIGEN and the deposit shares are calculated as normal.
* @param token token to be deposited, can be either EIGEN or bEIGEN. If EIGEN, then is unwrapped into bEIGEN
* @param amount deposit amount
*/
function _beforeDeposit(IERC20 token, uint256 amount) internal virtual override {
require(token == underlyingToken || token == EIGEN, "EigenStrategy.deposit: Can only deposit bEIGEN or EIGEN");

if (token == EIGEN) {
// unwrap EIGEN into bEIGEN assuming a 1-1 unwrapping amount
// the strategy will then hold `amount` of bEIGEN
EIGEN.unwrap(amount);
}
}

/**
* @notice This function hook is called in EigenStrategy.withdraw() before withdrawn shares are calculated and is
* overridden here to allow for withdrawing shares either into EIGEN or bEIGEN tokens. If wrapping bEIGEN into EIGEN is needed,
* it is performed in _afterWithdrawal(). This hook just checks the token paramater is either EIGEN or bEIGEN.
* @param token token to be withdrawn, can be either EIGEN or bEIGEN. If EIGEN, then bEIGEN is wrapped into EIGEN
*/
function _beforeWithdrawal(address /*recipient*/, IERC20 token, uint256 /*amountShares*/) internal virtual override {
require(token == underlyingToken || token == EIGEN, "EigenStrategy.withdraw: Can only withdraw bEIGEN or EIGEN");
}

/**
* @notice This function hook is called in EigenStrategy.withdraw() after withdrawn shares are calculated and is
* overridden here to allow for withdrawing shares either into EIGEN or bEIGEN tokens. If token is bEIGEN aka the underlyingToken,
* then the contract functions exactly the same as the StrategyBase contract and transfers out bEIGEN to the recipient.
* If token is EIGEN, then bEIGEN is first 1-1 wrapped into EIGEN and the strategy transfers out the EIGEN to the recipient.
* @param recipient recipient of the withdrawal
* @param token token to be withdrawn, can be either EIGEN or bEIGEN. If EIGEN, then bEIGEN is wrapped into EIGEN
* @param amountToSend amount of tokens to transfer
*/
function _afterWithdrawal(address recipient, IERC20 token, uint256 amountToSend) internal virtual override {
if (token == EIGEN) {
// wrap bEIGEN into EIGEN assuming a 1-1 wrapping amount
// the strategy will then hold `amountToSend` of EIGEN
underlyingToken.approve(address(token), amountToSend);
EIGEN.wrap(amountToSend);
}

// Whether the withdrawal specified EIGEN or bEIGEN, the strategy
// holds the correct balance and can transfer to the recipient here
token.safeTransfer(recipient, amountToSend);
}
Comment on lines +86 to +97

Check warning

Code scanning / Slither

Unused return Medium


/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
132 changes: 132 additions & 0 deletions src/contracts/token/BackingEigen.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity =0.8.12;
gpsanant marked this conversation as resolved.
Show resolved Hide resolved

import "@openzeppelin-v4.9.0/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin-upgrades-v4.9.0/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol";
import "@openzeppelin-upgrades-v4.9.0/contracts/access/OwnableUpgradeable.sol";

contract BackingEigen is OwnableUpgradeable, ERC20VotesUpgradeable {
/// CONSTANTS & IMMUTABLES
/// @notice the address of the wrapped Eigen token EIGEN
IERC20 public immutable EIGEN;

/// STORAGE
/// @notice the timestamp after which transfer restrictions are disabled
uint256 public transferRestrictionsDisabledAfter;
/// @notice mapping of addresses that are allowed to transfer tokens to any address
mapping(address => bool) public allowedFrom;
/// @notice mapping of addresses that are allowed to receive tokens from any address
mapping(address => bool) public allowedTo;

/// @notice event emitted when the allowedFrom status of an address is set
event SetAllowedFrom(address indexed from, bool isAllowedFrom);
/// @notice event emitted when the allowedTo status of an address is set
event SetAllowedTo(address indexed to, bool isAllowedTo);
/// @notice event emitted when the transfer restrictions are disabled
event TransferRestrictionsDisabled();
/// @notice event emitted when the EIGEN token is backed
event Backed();

constructor(IERC20 _EIGEN) {
EIGEN = _EIGEN;
_disableInitializers();
}

/**
* @notice An initializer function that sets initial values for the contract's state variables.
*/
function initialize(address initialOwner) initializer public {
__Ownable_init();
__ERC20_init("Backing Eigen", "bEIGEN");
_transferOwnership(initialOwner);
__ERC20Permit_init("bEIGEN");

// set transfer restrictions to be disabled at type(uint256).max to be set down later
transferRestrictionsDisabledAfter = type(uint256).max;

// the EIGEN contract should be allowed to transfer tokens to any address for unwrapping
// likewise, anyone should be able to transfer bEIGEN to EIGEN for wrapping
_setAllowedFrom(address(EIGEN), true);
_setAllowedTo(address(EIGEN), true);

// Mint the entire supply of EIGEN - this is a one-time event that
// ensures bEIGEN fully backs EIGEN.
_mint(address(EIGEN), EIGEN.totalSupply());
emit Backed();
}

/// EXTERNAL FUNCTIONS

/**
* @notice This function allows the owner to set the allowedFrom status of an address
* @param from the address whose allowedFrom status is being set
* @param isAllowedFrom the new allowedFrom status
*/
function setAllowedFrom(address from, bool isAllowedFrom) external onlyOwner {
_setAllowedFrom(from, isAllowedFrom);
}

/**
* @notice This function allows the owner to set the allowedTo status of an address
* @param to the address whose allowedTo status is being set
* @param isAllowedTo the new allowedTo status
*/
function setAllowedTo(address to, bool isAllowedTo) external onlyOwner {
_setAllowedTo(to, isAllowedTo);
}

/**
* @notice Allows the owner to disable transfer restrictions
*/
function disableTransferRestrictions() external onlyOwner {
require(transferRestrictionsDisabledAfter == type(uint256).max, "BackingEigen.disableTransferRestrictions: transfer restrictions are already disabled");
transferRestrictionsDisabledAfter = 0;
emit TransferRestrictionsDisabled();
}

/// VIEW FUNCTIONS

/**
* @dev Clock used for flagging checkpoints. Has been overridden to implement timestamp based
* checkpoints (and voting).
*/
function clock() public view override returns (uint48) {
return SafeCastUpgradeable.toUint48(block.timestamp);
}

/**
* @dev Machine-readable description of the clock as specified in EIP-6372.
* Has been overridden to inform callers that this contract uses timestamps instead of block numbers, to match `clock()`
*/
// solhint-disable-next-line func-name-mixedcase
function CLOCK_MODE() public pure override returns (string memory) {
return "mode=timestamp";
}

/// INTERNAL FUNCTIONS

function _setAllowedFrom(address from, bool isAllowedFrom) internal {
allowedFrom[from] = isAllowedFrom;
emit SetAllowedFrom(from, isAllowedFrom);
}

function _setAllowedTo(address to, bool isAllowedTo) internal {
allowedTo[to] = isAllowedTo;
emit SetAllowedTo(to, isAllowedTo);
}

/**
* @notice Overrides the beforeTokenTransfer function to enforce transfer restrictions
* @param from the address tokens are being transferred from
* @param to the address tokens are being transferred to
* @param amount the amount of tokens being transferred
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
// if transfer restrictions are enabled
if (block.timestamp <= transferRestrictionsDisabledAfter) {
// if both from and to are not whitelisted
require(allowedFrom[from] || allowedTo[to] || from == address(0), "BackingEigen._beforeTokenTransfer: from or to must be whitelisted");
}
super._beforeTokenTransfer(from, to, amount);
}
}
Loading
Loading