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

KEEP/NU <> T Vending Machine #3

Merged
merged 29 commits into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6dc1cd6
Vending Machine initial implementation
pdyraga Jul 21, 2021
6cb683d
Allow to unwrap only up to the previously wrapped amount
pdyraga Jul 21, 2021
e86e458
Added Solidity docs to VendingMachine contract
pdyraga Jul 21, 2021
aa000f3
Write to state variables before external function call
pdyraga Jul 21, 2021
7f02038
Merge branch 'main' into vending-machine
pdyraga Jul 30, 2021
579d082
thesis/solidity-contracts version updated
pdyraga Jul 30, 2021
743a57c
Set solidity compiler version to 0.8.4
pdyraga Jul 30, 2021
67eb566
Updated SPDX license identifier to GPL-3.0-or-later
pdyraga Jul 30, 2021
9a35c6a
Marked VendingMachine's ratio field as immutable
pdyraga Jul 30, 2021
44026d5
Generalize test amounts. Use a more real example
cygnusv Aug 1, 2021
7a4fbce
Update language of comments
cygnusv Aug 2, 2021
2ff3215
Calculate ratio in contract from its supplies
cygnusv Aug 2, 2021
d3356f4
Add public conversion functions
cygnusv Aug 2, 2021
f46ac7d
Missing linting
cygnusv Aug 2, 2021
ebb9971
Merge pull request #6 from cygnusv/vending-machine
pdyraga Aug 4, 2021
2799e56
Fixed typo in a test name
pdyraga Aug 4, 2021
ec01817
Updated parameter name for conversionToT/conversionFromT
pdyraga Aug 4, 2021
2003b3d
Renamed _maxWrappedToken to _wrappedTokenSuppy, documented ctor
pdyraga Aug 4, 2021
9b1190c
Restrict wrapping/unwrapping operations so only exact amounts are con…
cygnusv Aug 8, 2021
47b6b81
Additional tests for unexact conversions
cygnusv Aug 8, 2021
e9cc4dd
Add type restrictions to supply parameters to protect agains multipli…
cygnusv Aug 10, 2021
03f9c35
Add unit tests of public variables of VendingMachine contract
cygnusv Aug 10, 2021
d676823
VendingMachine doesn't need to be Ownable
cygnusv Aug 16, 2021
3310eb9
Check that token allocations are consistent with token supplies
cygnusv Aug 16, 2021
1cda4c1
Clarify conversion precision with an example
cygnusv Aug 16, 2021
c9e60b5
Restrict token allocations to uint96
cygnusv Aug 16, 2021
e311bd4
Disallow zero value conversions
cygnusv Aug 16, 2021
690d529
Use less exotic amounts in tests
cygnusv Aug 16, 2021
9746e04
Merge pull request #9 from cygnusv/vending-machine
cygnusv Aug 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions contracts/test/TestToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.4;

import "@thesis/solidity-contracts/contracts/token/ERC20WithPermit.sol";

contract TestToken is ERC20WithPermit {
constructor() ERC20WithPermit("Test Token", "TEST") {}
}
9 changes: 9 additions & 0 deletions contracts/token/T.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.4;

import "@thesis/solidity-contracts/contracts/token/ERC20WithPermit.sol";

contract T is ERC20WithPermit {
pdyraga marked this conversation as resolved.
Show resolved Hide resolved
constructor() ERC20WithPermit("Threshold Network Token", "T") {}
}
5 changes: 0 additions & 5 deletions contracts/token/TToken.sol

This file was deleted.

205 changes: 205 additions & 0 deletions contracts/vending/VendingMachine.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.4;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol";
import "../token/T.sol";

/// @title T token vending machine
/// @notice Contract implements a special update protocol to enable KEEP/NU
/// token holders to wrap their tokens and obtain T tokens according
/// to a fixed ratio. This will go on indefinitely and enable NU and
/// KEEP token holders to join T network without needing to buy or
/// sell any assets. Logistically, anyone holding NU or KEEP can wrap
/// those assets in order to receive T. They can also unwrap T in
/// order to go back to the underlying asset. There is a separate
/// instance of this contract deployed for KEEP holders and a separate
/// instance of this contract deployed for NU holders.
contract VendingMachine is IReceiveApproval {
using SafeERC20 for IERC20;
using SafeERC20 for T;

/// @notice Number of decimal places of precision in conversion to/from
/// wrapped tokens (assuming typical ERC20 token with 18 decimals).
/// This implies that amounts of wrapped tokens below this precision
/// won't take part in the conversion. E.g., for a value of 3, then
/// for a conversion of 1.123456789 wrapped tokens, only 1.123 is
/// convertible (i.e., 3 decimal places), and 0.000456789 is left.
uint256 public constant WRAPPED_TOKEN_CONVERSION_PRECISION = 3;

/// @notice Divisor for precision purposes, used to represent fractions.
uint256 public constant FLOATING_POINT_DIVISOR =
10**(18 - WRAPPED_TOKEN_CONVERSION_PRECISION);

/// @notice The token being wrapped to T (KEEP/NU).
IERC20 public immutable wrappedToken;

/// @notice T token contract.
T public immutable tToken;

/// @notice The ratio with which T token is converted based on the provided
/// token being wrapped (KEEP/NU), expressed in 1e18 precision.
///
/// When wrapping:
/// x [T] = amount [KEEP/NU] * ratio / FLOATING_POINT_DIVISOR
///
/// When unwrapping:
/// x [KEEP/NU] = amount [T] * FLOATING_POINT_DIVISOR / ratio
uint256 public immutable ratio;

/// @notice The total balance of wrapped tokens for the given holder
/// account. Only holders that have previously wrapped KEEP/NU to T
/// can unwrap, up to the amount previously wrapped.
mapping(address => uint256) public wrappedBalance;

event Wrapped(
address indexed recipient,
uint256 wrappedTokenAmount,
uint256 tTokenAmount
);
event Unwrapped(
address indexed recipient,
uint256 tTokenAmount,
uint256 wrappedTokenAmount
);
Copy link
Contributor

Choose a reason for hiding this comment

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

To make it perfect we can add docstrings for events too (and generate docs from contracts in some moment in a future)

Copy link
Member

Choose a reason for hiding this comment

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

Opened #11


/// @notice Sets the reference to `wrappedToken` and `tToken`. Initializes
/// conversion `ratio` between wrapped token and T based on the
/// provided `_tTokenAllocation` and `_wrappedTokenAllocation`.
/// @param _wrappedToken Address to ERC20 token that will be wrapped to T
/// @param _tToken Address of T token
/// @param _wrappedTokenAllocation The total supply of the token that will be
/// wrapped to T
/// @param _tTokenAllocation The allocation of T this instance of Vending
/// Machine will receive
/// @dev Multiplications in this contract can't overflow uint256 as we
/// restrict `_wrappedTokenAllocation` and `_tTokenAllocation` to
/// 96 bits and FLOATING_POINT_DIVISOR fits in less than 60 bits.
constructor(
IERC20 _wrappedToken,
T _tToken,
uint96 _wrappedTokenAllocation,
uint96 _tTokenAllocation
) {
require(
_tToken.totalSupply() >= _tTokenAllocation &&
_wrappedToken.totalSupply() >= _wrappedTokenAllocation,
"Allocations can't be greater than token supplies"
);
wrappedToken = _wrappedToken;
tToken = _tToken;
ratio =
(FLOATING_POINT_DIVISOR * _tTokenAllocation) /
_wrappedTokenAllocation;
}

/// @notice Wraps up to the the given `amount` of the token (KEEP/NU) and
/// releases T token proportionally to the amount being wrapped with
/// respect to the wrap ratio. The token holder needs to have at
/// least the given amount of the wrapped token (KEEP/NU) approved
/// to transfer to the Vending Machine before calling this function.
/// @param amount The amount of KEEP/NU to be wrapped
function wrap(uint256 amount) external {
_wrap(msg.sender, amount);
}

/// @notice Wraps up to the given amount of the token (KEEP/NU) and releases
/// T token proportionally to the amount being wrapped with respect
/// to the wrap ratio. This is a shortcut to `wrap` function that
/// avoids a separate approval transaction. Only KEEP/NU token
/// is allowed as a caller, so please call this function via
/// token's `approveAndCall`.
/// @param from Caller's address, must be the same as `wrappedToken` field
/// @param amount The amount of KEEP/NU to be wrapped
/// @param token Token's address, must be the same as `wrappedToken` field
function receiveApproval(
address from,
uint256 amount,
address token,
bytes calldata
) external override {
require(
token == address(wrappedToken),
"Token is not the wrapped token"
);
require(
msg.sender == address(wrappedToken),
"Only wrapped token caller allowed"
);
_wrap(from, amount);
}

/// @notice Unwraps up to the given `amount` of T back to the legacy token
/// (KEEP/NU) according to the wrap ratio. It can only be called by
/// a token holder who previously wrapped their tokens in this
/// vending machine contract. The token holder can't unwrap more
/// tokens than they originally wrapped. The token holder needs to
/// have at least the given amount of T tokens approved to transfer
/// to the Vending Machine before calling this function.
/// @param amount The amount of T to unwrap back to the collateral (KEEP/NU)
function unwrap(uint256 amount) external {
_unwrap(msg.sender, amount);
}

/// @notice Returns the T token amount that's obtained from `amount` wrapped
/// tokens (KEEP/NU), and the remainder that can't be converted.
function conversionToT(uint256 amount)
public
view
returns (uint256 tAmount, uint256 wrappedRemainder)
{
wrappedRemainder = amount % FLOATING_POINT_DIVISOR;
uint256 convertibleAmount = amount - wrappedRemainder;
tAmount = (convertibleAmount * ratio) / FLOATING_POINT_DIVISOR;
}

/// @notice The amount of wrapped tokens (KEEP/NU) that's obtained from
/// `amount` T tokens, and the remainder that can't be converted.
function conversionFromT(uint256 amount)
public
view
returns (uint256 wrappedAmount, uint256 tRemainder)
{
tRemainder = amount % ratio;
uint256 convertibleAmount = amount - tRemainder;
wrappedAmount = (convertibleAmount * FLOATING_POINT_DIVISOR) / ratio;
}

function _wrap(address tokenHolder, uint256 wrappedTokenAmount) internal {
(uint256 tTokenAmount, uint256 remainder) = conversionToT(
wrappedTokenAmount
);
wrappedTokenAmount -= remainder;
require(wrappedTokenAmount > 0, "Disallow conversions of zero value");
emit Wrapped(tokenHolder, wrappedTokenAmount, tTokenAmount);

wrappedBalance[tokenHolder] += wrappedTokenAmount;
wrappedToken.safeTransferFrom(
tokenHolder,
address(this),
wrappedTokenAmount
);
tToken.safeTransfer(tokenHolder, tTokenAmount);
}

function _unwrap(address tokenHolder, uint256 tTokenAmount) internal {
(uint256 wrappedTokenAmount, uint256 remainder) = conversionFromT(
tTokenAmount
);
tTokenAmount -= remainder;
require(tTokenAmount > 0, "Disallow conversions of zero value");
require(
wrappedBalance[tokenHolder] >= wrappedTokenAmount,
"Can not unwrap more than previously wrapped"
);

emit Unwrapped(tokenHolder, tTokenAmount, wrappedTokenAmount);
wrappedBalance[tokenHolder] -= wrappedTokenAmount;
tToken.safeTransferFrom(tokenHolder, address(this), tTokenAmount);
wrappedToken.safeTransfer(tokenHolder, wrappedTokenAmount);
}
}
2 changes: 1 addition & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ require("hardhat-gas-reporter")

module.exports = {
solidity: {
version: "0.8.6",
version: "0.8.4",
},
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@
"solhint": "^3.3.6",
"solhint-config-keep": "github:keep-network/solhint-config-keep",
"typescript": "^4.3.2"
},
"dependencies": {
"@thesis/solidity-contracts": "github:thesis/solidity-contracts#dc9f223"
}
}
19 changes: 19 additions & 0 deletions test/helpers/contract-test-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function to1e18(n) {
const decimalMultiplier = ethers.BigNumber.from(10).pow(18)
return ethers.BigNumber.from(n).mul(decimalMultiplier)
}

function to1ePrecision(n, precision) {
const decimalMultiplier = ethers.BigNumber.from(10).pow(precision)
return ethers.BigNumber.from(n).mul(decimalMultiplier)
}

async function getBlockTime(blockNumber) {
return (await ethers.provider.getBlock(blockNumber)).timestamp
}

module.exports.to1e18 = to1e18
module.exports.to1ePrecision = to1ePrecision
module.exports.getBlockTime = getBlockTime

module.exports.ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
7 changes: 0 additions & 7 deletions test/token/TToken.test.js

This file was deleted.

Loading