Skip to content

Commit

Permalink
feat: Initial approach to support nexus integration (#235)
Browse files Browse the repository at this point in the history
* 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
satyamakgec authored Jan 25, 2022
1 parent 65fddc8 commit 8856664
Show file tree
Hide file tree
Showing 10 changed files with 822 additions and 17 deletions.
4 changes: 2 additions & 2 deletions contracts/Tranche.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import "./libraries/DateString.sol";
contract Tranche is ERC20Permit, ITranche {
IInterestToken public immutable override interestToken;
IWrappedPosition public immutable position;
IERC20 public immutable underlying;
IERC20 public immutable override underlying;
uint8 internal immutable _underlyingDecimals;

// The outstanding amount of underlying which
Expand All @@ -25,7 +25,7 @@ contract Tranche is ERC20Permit, ITranche {
// The total supply of interest tokens
uint128 public override interestSupply;
// The timestamp when tokens can be redeemed.
uint256 public immutable unlockTimestamp;
uint256 public immutable override unlockTimestamp;
// The amount of slippage allowed on the Principal token redemption [0.1 basis points]
uint256 internal constant _SLIPPAGE_BP = 1e13;
// The speedbump variable records the first timestamp where redemption was attempted to be
Expand Down
236 changes: 236 additions & 0 deletions contracts/external/nexus/WrappedCoveredPrincipalToken.sol
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 contracts/external/nexus/WrappedCoveredPrincipalTokenFactory.sol
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");
}
}
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;
}
4 changes: 4 additions & 0 deletions contracts/interfaces/ITranche.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ interface ITranche is IERC20Permit {
function interestToken() external view returns (IInterestToken);

function interestSupply() external view returns (uint128);

function underlying() external view returns (IERC20);

function unlockTimestamp() external view returns (uint256);
}
Loading

0 comments on commit 8856664

Please sign in to comment.