From e2b00ca18d536658a33804734bd655a8e64340c6 Mon Sep 17 00:00:00 2001 From: jossduff Date: Wed, 27 Sep 2023 22:26:42 -0400 Subject: [PATCH] first draft --- contracts/PermissionedSet.sol | 334 +++++++++++--------------------- contracts/PermissionedToken.sol | 134 +++++++++++++ package.json | 1 + 3 files changed, 250 insertions(+), 219 deletions(-) create mode 100644 contracts/PermissionedToken.sol diff --git a/contracts/PermissionedSet.sol b/contracts/PermissionedSet.sol index 0747d9c..f4bf542 100644 --- a/contracts/PermissionedSet.sol +++ b/contracts/PermissionedSet.sol @@ -1,26 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.19; -import { - Ownable -} from "@openzeppelin/contracts/access/Ownable.sol"; -import { - ReentrancyGuard -} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import { - IERC20 -} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { - SafeERC20 -} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import { - EIP712 -} from "./lib/EIP712.sol"; - -error InvalidSignature (); -error AlreadyClaimed (); -error SweepingTransferFailed (); +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {EIP712} from "./lib/EIP712.sol"; + +error InvalidSignature(); /** @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM @@ -30,203 +14,115 @@ error SweepingTransferFailed (); @custom:date September 27th, 2023. */ -contract PermissionedSet is - EIP712, Ownable, ReentrancyGuard -{ - using SafeERC20 for IERC20; - - /// A constant hash of the claim operation's signature. - bytes32 constant public CLAIM_TYPEHASH = keccak256( - "claim(address _claimant,address _asset,uint256 _amount)" - ); - - /// The name of the vault. - string public name; - - /// The address permitted to sign claim signatures. - address public immutable signer; - - /// The address of the staker. - address public immutable staker; - - /// The address of the reward token. - address public immutable rewardToken; - - /// A mapping for whether or not a specific claimant has claimed. - mapping ( address => bool ) public claimed; - - /** - An event emitted when a claimant claims tokens. - - @param claimant The address of the user receiving the tokens. - @param caller The caller who claimed the tokens. - @param amount The amount of tokens claimed. - */ - event Claimed ( - address indexed claimant, - address indexed caller, - uint256 amount - ); - - /** - Construct a new vault by providing it a permissioned claim signer which may - issue claims and claim amounts. - - @param _name The name of the vault used in EIP-712 domain separation. - @param _signer The address permitted to sign claim signatures. - @param _staker The address of the Impostors staker. - @param _rewardToken The address of the reward Token. - */ - constructor ( - string memory _name, - address _signer, - address _staker, - address _rewardToken - ) EIP712(_name, "1") { - name = _name; - signer = _signer; - staker = _staker; - rewardToken = _rewardToken; - IERC20(rewardToken).approve(staker, type(uint256).max); - } - - /** - A private helper function to validate a signature supplied for token claims. - This function constructs a digest and verifies that the signature signer was - the authorized address we expect. - - @param _claimant The claimant attempting to claim tokens. - @param _asset The address of the ERC-20 token being claimed. - @param _amount The amount of tokens the claimant is trying to claim. - @param _v The recovery byte of the signature. - @param _r Half of the ECDSA signature pair. - @param _s Half of the ECDSA signature pair. - */ - function _validClaim ( - address _claimant, - address _asset, - uint256 _amount, - uint8 _v, - bytes32 _r, - bytes32 _s - ) private view returns (bool) { - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR, - keccak256( - abi.encode( - CLAIM_TYPEHASH, - _claimant, - _asset, - _amount - ) - ) - ) - ); - - // The claim is validated if it was signed by our authorized signer. - return ecrecover(digest, _v, _r, _s) == signer; - } - - /** - Allow a caller to claim any of their available tokens and automatically - stake them in the SuperVerseDAO staker if: - 1. the claim is backed by a valid signature from the trusted `signer`. - 2. the vault has enough tokens to fulfill the claim. - 3. the staker contract is approved to spend the correct tokens by the claimant - - @param _claimant The address of the user to claim tokens for. - @param _amount The amount of tokens that the caller is trying to claim. - @param _v The recovery byte of the signature. - @param _r Half of the ECDSA signature pair. - @param _s Half of the ECDSA signature pair. - */ - function claimAndStake ( - address _claimant, - uint256 _amount, - uint256 _provided, - uint8 _v, - bytes32 _r, - bytes32 _s - ) external nonReentrant { - - // Validate that the claimant has not already claimed. - if (claimed[_claimant]) { - revert AlreadyClaimed(); - } - - // Validiate that the claim was provided by our trusted `signer`. - bool validSignature = _validClaim( - _claimant, - rewardToken, - _amount, - _v, - _r, - _s - ); - if (!validSignature) { - revert InvalidSignature(); - } - - // Mark the claim as fulfilled. - claimed[_claimant] = true; - - // Transfer any provided tokens from the claimant. - IERC20(rewardToken).safeTransferFrom( - _claimant, - address(this), - _provided - ); - - // Stake the claim and any additional balance - ISuperVerseStaker.InputItem[] memory emptyItems; - ISuperVerseStaker(staker).stake( - _amount + _provided, - _claimant, - emptyItems - ); - - // Emit an event. - emit Claimed(_claimant, msg.sender, _amount); - } - - /** - Grant the SuperVerseDAO staker an allowance to transfer a specified - '_amount' of reward tokens - - @param _amount The amount of token to approve. - */ - function approveStaker ( - uint256 _amount - ) external onlyOwner { - IERC20(rewardToken).approve(staker, _amount); - } - - /** - Allow the owner to sweep either Ether or a particular ERC-20 token from the - contract and send it to another address. This allows the owner of the shop - to withdraw their funds after the sale is completed. - - @param _token The token to sweep the balance from; if a zero address is sent - then the contract's balance of Ether will be swept. - @param _destination The address to send the swept tokens to. - @param _amount The amount of token to sweep. - */ - function sweep ( - address _token, - address _destination, - uint256 _amount - ) external onlyOwner nonReentrant { - - // A zero address means we should attempt to sweep Ether. - if (_token == address(0)) { - (bool success, ) = payable(_destination).call{ value: _amount }(""); - if (!success) { revert SweepingTransferFailed(); } - - // Otherwise, we should try to sweep an ERC-20 token. - } else { - IERC20(_token).transfer(_destination, _amount); - } - } +contract PermissionedSet is EIP712, Ownable { + /// A constant hash of the claim operation's signature. + // same typehash for wrappping and unwrapping + // all we need in the signature is address, and optionally an array of people to whitelist storage + // update. And an array to remove. + bytes32 public constant PERMISSION_TYPEHASH = + keccak256( + "permission(address _caller,address[] _whitelist,address[] _blacklist)" + ); + + /// The name of the permission set + string public permissionedSetName; + + /// The address permitted to sign claim signatures. + address public immutable signer; + + /// A mapping for whitelisted accounts + mapping(address => bool) public whitelist; + + event WhitelistSet( + address indexed caller, + address[] whitelist, + address[] blacklist + ); + + constructor( + string memory _permissionedSetName, + address _signer + ) EIP712(_permissionedSetName, "1") { + permissionedSetName = _permissionedSetName; + signer = _signer; + } + + function _validatePermission( + address _caller, + address[] calldata _whitelist, + address[] calldata _blacklist, + uint8 _v, + bytes32 _r, + bytes32 _s + ) private view returns (bool) { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + PERMISSION_TYPEHASH, + _caller, + _whitelist, + _blacklist + ) + ) + ) + ); + + // The claim is validated if it was signed by our authorized signer. + return ecrecover(digest, _v, _r, _s) == signer; + } + + // called externally when owner has to update whitelist or blacklist + function set( + address[] calldata _whitelist, + address[] calldata _blacklist + ) external onlyOwner { + // add everyone on _whitelist to whitelist + for (uint i = 0; i < _whitelist.length; i++) { + whitelist[_whitelist[i]] = true; + } + + // remove everyone on _blacklist from whitelist + for (uint i = 0; i < _blacklist.length; i++) { + whitelist[_blacklist[i]] = false; + } + + emit WhitelistSet(msg.sender, _whitelist, _blacklist); + } + + // called when a user wraps or unwraps + function delegatedSet( + address[] calldata _whitelist, + address[] calldata _blacklist, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public { + // Validiate that the permission was provided by our trusted `signer`. + bool validSignature = _validatePermission( + msg.sender, + _whitelist, + _blacklist, + _v, + _r, + _s + ); + if (!validSignature) { + revert InvalidSignature(); + } + + // add everyone on _whitelist to whitelist + for (uint256 i = 0; i < _whitelist.length; i++) { + whitelist[_whitelist[i]] = true; + } + + // remove everyone on _blacklist from whitelist + for (uint256 i = 0; i < _blacklist.length; i++) { + whitelist[_blacklist[i]] = false; + } + + emit WhitelistSet(msg.sender, _whitelist, _blacklist); + } } diff --git a/contracts/PermissionedToken.sol b/contracts/PermissionedToken.sol new file mode 100644 index 0000000..0e52595 --- /dev/null +++ b/contracts/PermissionedToken.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {PermissionedSet} from "./PermissionedSet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +error NotWhitelisted(); +error InvalidInterestRate(); + +contract PermissionedToken is PermissionedSet, ReentrancyGuard, ERC20 { + using SafeERC20 for ERC20; + + // ERC-20 usdc + IERC20 public immutable usdc; + + // interest rate to set by owner + // interest rate scaled by 1e18 + // ex: interest rate of 0.055 (5.5%) is 55_000_000_000_000_000 + uint public interestRateMantissa; + + event NewInterestRateMantissa( + address indexed caller, + uint256 indexed newInterestRateMantissa + ); + + event Wrapped( + address indexed caller, + uint256 indexed usdcAmountIn, + uint256 indexed tokenAmountOut, + ); + + event Unwrapped( + address indexed caller, + uint256 indexed tokenAmountIn, + uint256 indexed usdcAmountOut, + ); + + constructor( + uint _initialInterestRateMantissa, + address _usdc, + string memory _tokenName, + string memory _tokenSymbol, + string memory _permissionedSetName, + address _signer + ) + PermissionedSet(_permissionedSetName, _signer) + ERC20(_tokenName, _tokenSymbol) + { + if (_initialInterestRateMantissa <= 0) { + revert InvalidInterestRate(); + } + interestRateMantissa = _initialInterestRateMantissa; + usdc = IERC20(_usdc); + } + + function wrap( + uint64 _usdcAmount, + address[] calldata _whitelist, + address[] calldata _blacklist, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external nonReentrant { + delegatedSet(_whitelist, _blacklist, _v, _r, _s); + + if (!whitelist[msg.sender]) { + revert NotWhitelisted(); + } + + // transfer in _usdcAmount of usdc + usdc.transferFrom(msg.sender, address(this), _usdcAmount); + + // TODO: verify math + // + // tokenMintAmount = _usdcAmount * (1-interestRate) + // ex: usdcAmount = 100, interestRate = 0.05 (5%) + // tokenMintAmount = 100 * 0.95 = 95 + + // TODO: do we lose precision by dividing by 1e18?? What if interest rate is 5.5%? then tokenMintAmount = 100 * 0.945 = 94.5 + + uint256 tokenMintAmount = (_usdcAmount * 1e18 * (1e18 - interestRateMantissa)) / 1e18; + + // mint tokens to caller + _mint(msg.sender, tokenMintAmount); + + emit Wrapped(msg.sender, _usdcAmount, tokenMintAmount); + } + + function unwrap( + uint64 _tokenAmount, + address[] calldata _whitelist, + address[] calldata _blacklist, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external nonReentrant { + delegatedSet(_whitelist, _blacklist, _v, _r, _s); + + if (!whitelist[msg.sender]) { + revert NotWhitelisted(); + } + + // burn tokens + _burn(msg.sender, _tokenAmount); + + // TODO: verify math + // + // usdcAmount = _tokenAmount * (1 + interestRate) + // ex: _tokenAmount = 100, interestRate = 0.05 (5%) + // usdcAmount = 100 * 1.05 = 105 + uint256 usdcAmount = (_tokenAmount * + 1e18 * + (1e18 + interestRateMantissa)) / 1e18; + + // transfer out USDC + usdc.transfer(msg.sender, usdcAmount); + + emit Unwrap(msg.sender, _tokenAmount, usdcAmount); + } + + function setInterestRateMantissa( + uint _interestRateMantissa + ) external onlyOwner { + if (_interestRateMantissa <= 0) { + revert InvalidInterestRate(); + } + interestRateMantissa = _interestRateMantissa; + + emit NewInterestRateMantissa(msg.sender, _interestRateMantissa); + } +} diff --git a/package.json b/package.json index 7c5080c..a7017ad 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@babel/runtime": "^7.11.2", "@nomiclabs/hardhat-ethers": "^2.0.0", "@nomiclabs/hardhat-etherscan": "^3.1.7", + "@nomiclabs/hardhat-waffle": "^2.0.6", "@openzeppelin/contracts": "^4.3.1", "chai": "^4.2.0", "dotenv": "^8.2.0",