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 18 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.

169 changes: 169 additions & 0 deletions contracts/vending/VendingMachine.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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 Ownable, IReceiveApproval {
using SafeERC20 for IERC20;
using SafeERC20 for T;

/// @notice Divisor for precision purposes, used to represent fractions.
uint256 public constant FLOATING_POINT_DIVISOR = 1e18;

/// @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


/// @dev Sets the reference to `wrappedToken` and `tToken`. Initializes
/// conversion ratio between wrapped token and T based on the provided
/// `_tTokenAllocation` and `_wrappedTokenSupply`.
/// @param _wrappedToken Address to ERC20 token that will be wrapped to T
/// @param _tToken Address of T token
/// @param _wrappedTokenSupply 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
constructor(
IERC20 _wrappedToken,
T _tToken,
uint256 _wrappedTokenSupply,
uint256 _tTokenAllocation
) {
wrappedToken = _wrappedToken;
tToken = _tToken;
ratio =
(FLOATING_POINT_DIVISOR * _tTokenAllocation) /
_wrappedTokenSupply;
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about adding a few required for the input values? I know we control these parameters, but it costs nothing in terms of gas / code size.

}

/// @notice Wraps the given amount of the token (KEEP/NU) and
/// releases T token proportionally to the amount being wrapped and
/// 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 the given amount of the token (KEEP/NU) and
/// releases T token proportionally to the amount being wrapped and
/// the wrap ratio. This is a shortcut to `wrap` function allowing
/// to avoid 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 the given amount of T back to the legacy token (KEEP/NU)
/// based on the wrap ratio. Can only be called by a token holder
/// who previously wrapped their tokens. The token holder can not
/// 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 The T token amount that's obtained from `amount` wrapped
/// tokens (KEEP/NU).
function conversionToT(uint256 amount) public view returns (uint256) {
return (amount * ratio) / FLOATING_POINT_DIVISOR;
}

/// @notice The amount of wrapped tokens (KEEP/NU) than's obtained from
/// `amount` T tokens.
function conversionFromT(uint256 amount) public view returns (uint256) {
return (amount * FLOATING_POINT_DIVISOR) / ratio;
}

function _wrap(address tokenHolder, uint256 wrappedTokenAmount) internal {
uint256 tTokenAmount = conversionToT(wrappedTokenAmount);
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 = conversionFromT(tTokenAmount);

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