-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Initial approach to support nexus integration (#235)
* feat: initial approach for the wrapped future principal token * fix compilation * feat: minor fixes * rename the contract * cosmetic change * use permit to mint * add test suite * complete test suite * used wrapped position instead of tranche * interface fixed * test suite fixes * minor fixes * fix test suite
- Loading branch information
1 parent
65fddc8
commit 8856664
Showing
10 changed files
with
822 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
236 changes: 236 additions & 0 deletions
236
contracts/external/nexus/WrappedCoveredPrincipalToken.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity 0.8.0; | ||
|
||
import { ERC20PermitWithSupply, ERC20Permit, IERC20Permit } from "../../libraries/ERC20PermitWithSupply.sol"; | ||
import { IWrappedPosition } from "../../interfaces/IWrappedPosition.sol"; | ||
import { ITranche } from "../../interfaces/ITranche.sol"; | ||
import { IWrappedCoveredPrincipalToken } from "./interfaces/IWrappedCoveredPrincipalToken.sol"; | ||
import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; | ||
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; | ||
|
||
/// @author Element Finance | ||
/// @title WrappedCoveredPrincipalToken | ||
contract WrappedCoveredPrincipalToken is | ||
ERC20PermitWithSupply, | ||
AccessControl, | ||
IWrappedCoveredPrincipalToken | ||
{ | ||
using EnumerableSet for EnumerableSet.AddressSet; | ||
using SafeERC20 for IERC20; | ||
|
||
// Address of the base/underlying token which is used to buy the yield bearing token from the wrapped position. | ||
// Ex - Dai is used to buy the yvDai yield bearing token | ||
address public immutable override baseToken; | ||
|
||
// Enumerable address list, It contains the list of allowed wrapped positions that are covered by this contract | ||
// Criteria to choose the wrapped position are - | ||
// a). Wrapped position should have same underlying/base token (i.e ETH, BTC, USDC). | ||
// b). Should have the similar risk profiles. | ||
EnumerableSet.AddressSet private _allowedWrappedPositions; | ||
|
||
// Tranche factory address for Tranche contract address derivation | ||
address internal immutable _trancheFactory; | ||
// Tranche bytecode hash for Tranche contract address derivation. | ||
// This is constant as long as Tranche does not implement non-constant constructor arguments. | ||
bytes32 internal immutable _trancheBytecodeHash; | ||
|
||
// Role identifier that can use to do some operational stuff. | ||
bytes32 public constant ADMIN_ROLE = bytes32("ADMIN_ROLE"); | ||
|
||
// Role identifier that allow a particular account to reap principal tokens out of the contract. | ||
bytes32 public constant RECLAIM_ROLE = bytes32("RECLAIM_ROLE"); | ||
|
||
// Emitted when new wrapped position get whitelisted. | ||
event WrappedPositionAdded(address _wrappedPosition); | ||
|
||
// Emitted when the principal tokens get reclaimed. | ||
event Reclaimed(address _tranche, uint256 _amount); | ||
|
||
/// @notice Modifier to validate the wrapped position is whitelisted or not. | ||
modifier isValidWp(address _wrappedPosition) { | ||
require(!isAllowedWp(_wrappedPosition), "WFP:ALREADY_EXISTS"); | ||
_; | ||
} | ||
|
||
///@notice Initialize the wrapped token. | ||
///@dev Wrapped token have 18 decimals, It is independent of the baseToken decimals. | ||
constructor( | ||
address _baseToken, | ||
address _owner, | ||
address __trancheFactory, | ||
bytes32 __trancheBytecodeHash | ||
) | ||
ERC20Permit( | ||
_processName(IERC20Metadata(_baseToken).symbol()), | ||
_processSymbol(IERC20Metadata(_baseToken).symbol()) | ||
) | ||
{ | ||
baseToken = _baseToken; | ||
_trancheFactory = __trancheFactory; | ||
_trancheBytecodeHash = __trancheBytecodeHash; | ||
_setupRole(ADMIN_ROLE, _owner); | ||
_setRoleAdmin(ADMIN_ROLE, ADMIN_ROLE); | ||
_setRoleAdmin(RECLAIM_ROLE, ADMIN_ROLE); | ||
} | ||
|
||
///@notice Allows to create the name for the wrapped token. | ||
function _processName(string memory _tokenSymbol) | ||
internal | ||
pure | ||
returns (string memory) | ||
{ | ||
return | ||
string( | ||
abi.encodePacked("Wrapped", _tokenSymbol, "Covered Principal") | ||
); | ||
} | ||
|
||
///@notice Allows to create the symbol for the wrapped token. | ||
function _processSymbol(string memory _tokenSymbol) | ||
internal | ||
pure | ||
returns (string memory) | ||
{ | ||
return string(abi.encodePacked("W", _tokenSymbol)); | ||
} | ||
|
||
/// @notice Add wrapped position within the allowed wrapped position enumerable set. | ||
/// @dev It is only allowed to execute by the owner of the contract. | ||
/// wrapped position which has underlying token equals to the base token are | ||
/// only allowed to add, Otherwise it will revert. | ||
/// @param _wrappedPosition Address of the Wrapped position which needs to add. | ||
function addWrappedPosition(address _wrappedPosition) | ||
external | ||
override | ||
isValidWp(_wrappedPosition) | ||
onlyRole(ADMIN_ROLE) | ||
{ | ||
require( | ||
address(IWrappedPosition(_wrappedPosition).token()) == baseToken, | ||
"WFP:INVALID_WP" | ||
); | ||
_allowedWrappedPositions.add(_wrappedPosition); | ||
emit WrappedPositionAdded(_wrappedPosition); | ||
} | ||
|
||
/// @notice Allows the defaulter to mint wrapped tokens (Covered position) by | ||
/// sending the de-pegged token to the contract. | ||
/// @dev a) Only allow minting the covered position when the derived tranche got expired otherwise revert. | ||
/// b) Sufficient allowance of the principal token (i.e tranche) should be provided | ||
/// to the contract by the `msg.sender` to make execution successful. | ||
/// @param _amount Amount of covered position / wrapped token `msg.sender` wants to mint. | ||
/// @param _expiration Timestamp at which the derived tranche would get expired. | ||
/// @param _wrappedPosition Address of the Wrapped position which is used to derive the tranche. | ||
function mint( | ||
uint256 _amount, | ||
uint256 _expiration, | ||
address _wrappedPosition, | ||
PermitData calldata _permitCallData | ||
) external override { | ||
require(isAllowedWp(_wrappedPosition), "WFP:INVALID_WP"); | ||
address _tranche = address( | ||
_deriveTranche(_wrappedPosition, _expiration) | ||
); | ||
_usePermitData(_tranche, _permitCallData); | ||
// Only allow minting when the position get expired. | ||
require(_expiration < block.timestamp, "WFP:POSITION_NOT_EXPIRED"); | ||
// Assumed that msg.sender provides the sufficient approval the contract. | ||
IERC20(_tranche).safeTransferFrom( | ||
msg.sender, | ||
address(this), | ||
_fromWad(_amount, _tranche) | ||
); | ||
// Mint the corresponding wrapped token to the `msg.sender`. | ||
_mint(msg.sender, _amount); | ||
} | ||
|
||
/// @notice Tell whether the given `_wrappedPosition` is whitelisted or not. | ||
/// @param _wrappedPosition Address of the wrapped position. | ||
/// @return returns boolean, True -> allowed otherwise false. | ||
function isAllowedWp(address _wrappedPosition) | ||
public | ||
view | ||
override | ||
returns (bool) | ||
{ | ||
return _allowedWrappedPositions.contains(_wrappedPosition); | ||
} | ||
|
||
/// @notice Returns the list of wrapped positions that are whitelisted with the contract. | ||
/// Order is not maintained. | ||
/// @return Array of addresses. | ||
function allWrappedPositions() | ||
external | ||
view | ||
override | ||
returns (address[] memory) | ||
{ | ||
return _allowedWrappedPositions.values(); | ||
} | ||
|
||
/// @notice Reclaim tranche token (i.e principal token) by the authorized account. | ||
/// @dev Only be called by the address which has the `RECLAIM_ROLE`, Should be Nexus Treasury. | ||
/// @param _expiration Timestamp at which the derived tranche would get expired. | ||
/// @param _wrappedPosition Address of the Wrapped position which is used to derive the tranche. | ||
/// @param _to Address whom funds gets transferred. | ||
function reclaimPt( | ||
uint256 _expiration, | ||
address _wrappedPosition, | ||
address _to | ||
) external override onlyRole(RECLAIM_ROLE) { | ||
require(isAllowedWp(_wrappedPosition), "WFP:INVALID_WP"); | ||
address _tranche = address( | ||
_deriveTranche(_wrappedPosition, _expiration) | ||
); | ||
uint256 amount = IERC20(_tranche).balanceOf(address(this)); | ||
IERC20(_tranche).safeTransfer(_to, amount); | ||
emit Reclaimed(_tranche, amount); | ||
} | ||
|
||
function _usePermitData(address _tranche, PermitData memory _d) internal { | ||
if (_d.spender != address(0)) { | ||
IERC20Permit(_tranche).permit( | ||
msg.sender, | ||
_d.spender, | ||
_d.value, | ||
_d.deadline, | ||
_d.v, | ||
_d.r, | ||
_d.s | ||
); | ||
} | ||
} | ||
|
||
/// @notice Converts the decimal precision of given `_amount` to `_tranche` decimal. | ||
function _fromWad(uint256 _amount, address _tranche) | ||
internal | ||
view | ||
returns (uint256) | ||
{ | ||
return (_amount * 10**IERC20Metadata(_tranche).decimals()) / 1e18; | ||
} | ||
|
||
/// @dev This internal function produces the deterministic create2 | ||
/// address of the Tranche contract from a wrapped position contract and expiration | ||
/// @param _position The wrapped position contract address | ||
/// @param _expiration The expiration time of the tranche | ||
/// @return The derived Tranche contract | ||
function _deriveTranche(address _position, uint256 _expiration) | ||
internal | ||
view | ||
returns (ITranche) | ||
{ | ||
bytes32 salt = keccak256(abi.encodePacked(_position, _expiration)); | ||
bytes32 addressBytes = keccak256( | ||
abi.encodePacked( | ||
bytes1(0xff), | ||
_trancheFactory, | ||
salt, | ||
_trancheBytecodeHash | ||
) | ||
); | ||
return ITranche(address(uint160(uint256(addressBytes)))); | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
contracts/external/nexus/WrappedCoveredPrincipalTokenFactory.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity 0.8.0; | ||
|
||
import { WrappedCoveredPrincipalToken, EnumerableSet } from "./WrappedCoveredPrincipalToken.sol"; | ||
|
||
/// @author Element Finance | ||
/// @title WrappedCoveredPrincipalTokenFactory | ||
contract WrappedCoveredPrincipalTokenFactory { | ||
using EnumerableSet for EnumerableSet.AddressSet; | ||
|
||
// Enumerable list of wrapped tokens that get created from the factory. | ||
EnumerableSet.AddressSet private _WrappedCoveredPrincipalTokens; | ||
|
||
// Tranche factory address for Tranche contract address derivation | ||
address internal immutable _trancheFactory; | ||
// Tranche bytecode hash for Tranche contract address derivation. | ||
// This is constant as long as Tranche does not implement non-constant constructor arguments. | ||
bytes32 internal immutable _trancheBytecodeHash; | ||
|
||
// Emitted when new wrapped principal token get created. | ||
event WrappedCoveredPrincipalTokenCreated( | ||
address indexed _baseToken, | ||
address indexed _owner | ||
); | ||
|
||
/// @notice Initializing the owner of the contract. | ||
constructor(address __trancheFactory, bytes32 __trancheBytecodeHash) { | ||
_trancheFactory = __trancheFactory; | ||
_trancheBytecodeHash = __trancheBytecodeHash; | ||
} | ||
|
||
/// @notice Allow the owner to create the new wrapped token. | ||
/// @param _baseToken Address of the base token / underlying token that is used to buy the wrapped positions. | ||
/// @param _owner Address of the owner of wrapped futures. | ||
/// @return address of wrapped futures token. | ||
function create(address _baseToken, address _owner) | ||
external | ||
returns (address) | ||
{ | ||
// Validate the given params | ||
_zeroAddressCheck(_owner); | ||
_zeroAddressCheck(_baseToken); | ||
address wcPrincipal = address( | ||
new WrappedCoveredPrincipalToken( | ||
_baseToken, | ||
_owner, | ||
_trancheFactory, | ||
_trancheBytecodeHash | ||
) | ||
); | ||
_WrappedCoveredPrincipalTokens.add(wcPrincipal); | ||
emit WrappedCoveredPrincipalTokenCreated(_baseToken, _owner); | ||
return wcPrincipal; | ||
} | ||
|
||
/// @notice Returns the list of wrapped tokens that are whitelisted with the contract. | ||
/// Order is not maintained. | ||
/// @return Array of addresses. | ||
function allWrappedCoveredPrincipalTokens() | ||
public | ||
view | ||
returns (address[] memory) | ||
{ | ||
return _WrappedCoveredPrincipalTokens.values(); | ||
} | ||
|
||
/// @notice Sanity check for the zero address check. | ||
function _zeroAddressCheck(address _target) internal pure { | ||
require(_target != address(0), "WFPF:ZERO_ADDRESS"); | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
contracts/external/nexus/interfaces/IWrappedCoveredPrincipalToken.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity 0.8.0; | ||
|
||
import { IERC20Permit } from "../../../interfaces/IERC20Permit.sol"; | ||
|
||
interface IWrappedCoveredPrincipalToken is IERC20Permit { | ||
// Memory encoding of the permit data | ||
struct PermitData { | ||
address spender; | ||
uint256 value; | ||
uint256 deadline; | ||
uint8 v; | ||
bytes32 r; | ||
bytes32 s; | ||
} | ||
|
||
// Address of the base/underlying token which is used to buy the yield bearing token from the wrapped position. | ||
// Ex - Dai is used to buy the yvDai yield bearing token. | ||
function baseToken() external view returns (address); | ||
|
||
/// @notice Add wrapped position within the allowed wrapped position enumerable set. | ||
/// @dev It is only allowed to execute by the owner of the contract. | ||
/// wrapped position which has underlying token equals to the base token are | ||
/// only allowed to add, Otherwise it will revert. | ||
/// @param _wrappedPosition Address of the Wrapped position which needs to add. | ||
function addWrappedPosition(address _wrappedPosition) external; | ||
|
||
/// @notice Allows the defaulter to mint wrapped tokens (Covered position) by | ||
/// sending the de-pegged token to the contract. | ||
/// @dev a) Only allow minting the covered position when the derived tranche got expired otherwise revert. | ||
/// b) Sufficient allowance of the principal token (i.e tranche) should be provided | ||
/// to the contract by the `msg.sender` to make execution successful. | ||
/// @param _amount Amount of covered position / wrapped token `msg.sender` wants to mint. | ||
/// @param _expiration Timestamp at which the derived tranche would get expired. | ||
/// @param _wrappedPosition Address of the Wrapped position which is used to derive the tranche. | ||
function mint( | ||
uint256 _amount, | ||
uint256 _expiration, | ||
address _wrappedPosition, | ||
PermitData calldata _permitCallData | ||
) external; | ||
|
||
/// @notice Tell whether the given `_wrappedPosition` is whitelisted or not. | ||
/// @param _wrappedPosition Address of the wrapped position. | ||
/// @return returns boolean, True -> allowed otherwise false. | ||
function isAllowedWp(address _wrappedPosition) external view returns (bool); | ||
|
||
/// @notice Returns the list of wrapped positions that are whitelisted with the contract. | ||
/// Order is not maintained. | ||
/// @return Array of addresses. | ||
function allWrappedPositions() external view returns (address[] memory); | ||
|
||
/// @notice Reclaim tranche token (i.e principal token) by the authorized account. | ||
/// @dev Only be called by the address which has the `RECLAIM_ROLE`, Should be Nexus Treasury. | ||
/// @param _expiration Timestamp at which the derived tranche would get expired. | ||
/// @param _wrappedPosition Address of the Wrapped position which is used to derive the tranche. | ||
/// @param _to Address whom funds gets transferred. | ||
function reclaimPt( | ||
uint256 _expiration, | ||
address _wrappedPosition, | ||
address _to | ||
) external; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.