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

Rework to the wrapped token approach #9

Merged
merged 3 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ build/
.env
.idea/
node_modules/

coverage/
coverage.json
2 changes: 2 additions & 0 deletions .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"reason-string": "off",
"no-inline-assembly": "off",
"no-complex-fallback": "off",
"avoid-low-level-calls": "off",
"no-empty-blocks": "off",
"func-visibility": ["warn", {"ignoreConstructors": true}],
"prettier/prettier": "error",
"compiler-version": ["error", "0.8.7"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@

pragma solidity 0.8.7;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "./interfaces/IDepositContract.sol";
import "./interfaces/IERC20.sol";
import "./interfaces/IERC165.sol";
import "./interfaces/IERC677Receiver.sol";
import "./utils/EIP1967Admin.sol";
import "./utils/PausableEIP1967Admin.sol";
import "./utils/Claimable.sol";

/**
* @title StakeDepositContract
* @title SBCDepositContract
* @dev Implementation of the ERC20 ETH2.0 deposit contract.
* For the original implementation, see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs
*/
contract StakeDepositContract is IDepositContract, IERC165, IERC677Receiver, EIP1967Admin, Claimable {
contract SBCDepositContract is IDepositContract, IERC165, IERC677Receiver, PausableEIP1967Admin, Claimable {
uint256 private constant DEPOSIT_CONTRACT_TREE_DEPTH = 32;
// NOTE: this also ensures `deposit_count` will fit into 64-bits
uint256 private constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1;
Expand All @@ -24,24 +23,12 @@ contract StakeDepositContract is IDepositContract, IERC165, IERC677Receiver, EIP
bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] private branch;
uint256 private deposit_count;

bool public paused;

IERC20 public immutable stake_token;

constructor(address _token) {
stake_token = IERC20(_token);
}

modifier whenNotPaused() {
require(!paused);
_;
}

function setPaused(bool _paused) external onlyAdmin {
require(_paused != paused);
paused = _paused;
}

function get_deposit_root() external view override returns (bytes32) {
bytes32 node;
uint256 size = deposit_count;
Expand Down Expand Up @@ -101,14 +88,14 @@ contract StakeDepositContract is IDepositContract, IERC165, IERC677Receiver, EIP
uint256 stake_amount,
bytes calldata data
) external override whenNotPaused returns (bool) {
require(msg.sender == address(stake_token));
require(msg.sender == address(stake_token), "DepositContract: not a deposit token");
require(data.length % 176 == 32, "DepositContract: incorrect deposit data length");
uint256 count = data.length / 176;
require(count > 0, "BatchDeposit: You should deposit at least one validator");
uint256 stake_amount_per_deposit = stake_amount;
if (count > 1) {
require(count <= 128, "BatchDeposit: You can deposit max 128 validators at a time");
require(stake_amount == 32 ether * count, "BatchDeposit: batch deposits require 32 STAKE deposit amount");
require(stake_amount == 32 ether * count, "BatchDeposit: batch deposits require 32 SBC deposit amount");
stake_amount_per_deposit = 32 ether;
}

Expand Down Expand Up @@ -203,10 +190,10 @@ contract StakeDepositContract is IDepositContract, IERC165, IERC677Receiver, EIP
* Only admin can call this method.
* Deposit-related tokens cannot be claimed.
* @param _token address of the token, if it is not provided (0x00..00), native coins will be transferred.
* @param _to address that will receive the locked tokens on this contract.
* @param _to address that will receive the locked tokens from this contract.
*/
function claimTokens(address _token, address _to) external onlyAdmin {
require(address(stake_token) != _token);
require(address(stake_token) != _token, "DepositContract: not allowed to claim deposit token");
_claimValues(_token, _to);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
pragma solidity 0.8.7;

import "./utils/EIP1967Proxy.sol";
import "./StakeDepositContract.sol";
import "./SBCDepositContract.sol";

/**
* @title StakeDepositContractProxy
* @dev Upgradeable and Claimable version of the underlying StakeDepositContract.
* @title SBCDepositContractProxy
* @dev Upgradeable version of the underlying SBCDepositContract.
*/
contract StakeDepositContractProxy is EIP1967Proxy {
contract SBCDepositContractProxy is EIP1967Proxy {
bool private paused;

uint256 private constant DEPOSIT_CONTRACT_TREE_DEPTH = 32;
// first slot from StakeDepositContract
bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] private zero_hashes;

constructor(address _admin, address _token) {
_setAdmin(_admin);
_setImplementation(address(new StakeDepositContract(_token)));
_setImplementation(address(new SBCDepositContract(_token)));

// Compute hashes in empty sparse Merkle tree
for (uint256 height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH - 1; height++)
Expand Down
67 changes: 67 additions & 0 deletions contracts/SBCToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.7;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "./utils/Claimable.sol";
import "./utils/PausableEIP1967Admin.sol";
import "./interfaces/IERC677.sol";
import "./interfaces/IERC677Receiver.sol";

/**
* @title SBCToken
* @dev Wrapped token used for depositing into SBC.
*/
contract SBCToken is IERC677, ERC20Pausable, PausableEIP1967Admin, Claimable {
address private _minter;

constructor() ERC20("", "") {}

/**
* @dev Initialization setter for the minter address.
* Only admin can call this method.
* @param minter address of the SBCWrapper contract.
*/
function setMinter(address minter) external onlyAdmin {
require(_minter == address(0), "SBCToken: minter already set");
_minter = minter;
}

/**
* @dev Mints new tokens.
* Only configured minter is allowed to mint tokens, which should be a SBCWrapper contract.
* @param _to tokens receiver.
* @param _amount amount of tokens to mint.
*/
function mint(address _to, uint256 _amount) external {
require(_msgSender() == _minter, "SBCToken: not a minter");
_mint(_to, _amount);
}

/**
* @dev Implements the ERC677 transferAndCall standard.
* Executes a regular transfer, but calls the receiver's function to handle them in the same transaction.
* @param _to tokens receiver.
* @param _amount amount of sent tokens.
* @param _data extra data to pass to the callback function.
*/
function transferAndCall(
address _to,
uint256 _amount,
bytes calldata _data
) external override {
address sender = _msgSender();
_transfer(sender, _to, _amount);
require(IERC677Receiver(_to).onTokenTransfer(sender, _amount, _data), "SBCToken: ERC677 callback failed");
}

/**
* @dev Allows to transfer any locked token from this contract.
* Only admin can call this method.
* @param _token address of the token, if it is not provided (0x00..00), native coins will be transferred.
* @param _to address that will receive the locked tokens from this contract.
*/
function claimTokens(address _token, address _to) external onlyAdmin {
_claimValues(_token, _to);
}
}
32 changes: 32 additions & 0 deletions contracts/SBCTokenProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.7;

import "./utils/EIP1967Proxy.sol";
import "./SBCToken.sol";

/**
* @title SBCTokenProxy
* @dev Upgradeable version of the underlying SBCToken.
*/
contract SBCTokenProxy is EIP1967Proxy {
mapping(address => uint256) private _balances;

mapping(address => mapping(address => uint256)) private _allowances;

uint256 private _totalSupply;

string private _name;
string private _symbol;

constructor(
address _admin,
string memory name,
string memory symbol
) {
_setAdmin(_admin);
_setImplementation(address(new SBCToken()));
_name = name;
_symbol = symbol;
}
}
141 changes: 141 additions & 0 deletions contracts/SBCWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.7;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./utils/PausableEIP1967Admin.sol";
import "./SBCToken.sol";

/**
* @title SBCWrapper
* @dev Wrapper engine contract for minting wrapped tokens that can be deposited into SBC.
* Used for wrapping of STAKE and other possible ERC20 tokens.
*/
contract SBCWrapper is IERC677Receiver, PausableEIP1967Admin, Claimable, ReentrancyGuard {
using SafeERC20 for IERC20;

enum TokenStatus {
DISABLED,
ENABLED,
PAUSED
}

mapping(address => TokenStatus) public tokenStatus;
// if tokenRate[A] = X, then user will receive Y * X / 10**18 wrapped tokens for locking Y of A tokens.
mapping(address => uint256) public tokenRate;

SBCToken public immutable sbcToken;

event Swap(address indexed token, address indexed user, uint256 amount, uint256 received);
event UpdateSwapRate(address indexed token, uint256 rate);

constructor(SBCToken _sbcToken) {
sbcToken = _sbcToken;
}

/**
* @dev Enables swapping of new token into wrapped SBC token at a given rate.
* Only admin can call this method.
* @param _token address of the enabled or reenabled token contract.
* @param _rate exchange rate for the new pair, multiplied by 10**18.
*/
function enableToken(address _token, uint256 _rate) external onlyAdmin {
require(_rate > 0, "SBCWrapper: invalid rate");

tokenStatus[_token] = TokenStatus.ENABLED;
tokenRate[_token] = _rate;

emit UpdateSwapRate(_token, _rate);
}

/**
* @dev Temporary pauses swapping of some particular token, which can be reenaled later.
* Only admin can call this method.
* @param _token address of the paused token contract.
*/
function pauseToken(address _token) external onlyAdmin {
tokenStatus[_token] = TokenStatus.PAUSED;
}

/**
* @dev Swaps some of the whitelisted tokens for the newly created wrapped tokens.
* Tokens must be pre-approved before calling this function.
* @param _token address of the swapped token contract.
* @param _amount amount of tokens to swap.
* @param _permitData optional permit calldata to use for preliminary token approval.
* supports STAKE permit and EIP2612 standards.
*/
function swap(
address _token,
uint256 _amount,
bytes calldata _permitData
) external nonReentrant whenNotPaused {
require(tokenStatus[_token] == TokenStatus.ENABLED, "SBCWrapper: token is not enabled");

if (_permitData.length > 4) {
// supported signatures:
// permit(address,address,uint256,uint256,bool,uint8,bytes32,bytes32)
// permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
require(
bytes4(_permitData[0:4]) == bytes4(0x8fcbaf0c) || bytes4(_permitData[0:4]) == bytes4(0xd505accf),
"SBCWrapper: invalid permit signature"
);
(bool status, ) = _token.call(_permitData);
require(status, "SBCWrapper: permit failed");
}

address sender = _msgSender();

// We do not plan to support any deflationary or rebasing tokens in this contract
// so it is not required to check that ERC20 balance has indeed change.
// It is an admin responsibility to carefully check that enabled token correctly implements ERC20 standard.
IERC20(_token).safeTransferFrom(sender, address(this), _amount);
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider either to make the code safer by adding a check that the swap contract balance increased by expected amount of tokens or to extend the code with a comment that this check is not here because the contract admin must be sure that such tokens are not added for swap.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, added a comment


_swapTokens(sender, _token, _amount);
}

/**
* @dev ERC677 callback for swapping tokens in the simpler way during transferAndCall.
* @param from address of the received token contract.
* @param value amount of the received tokens.
*/
function onTokenTransfer(
address from,
uint256 value,
bytes calldata
) external override nonReentrant whenNotPaused returns (bool) {
address token = _msgSender();
require(tokenStatus[token] == TokenStatus.ENABLED, "SBCWrapper: token is not enabled");

_swapTokens(from, token, value);
return true;
}

/**
* @dev Allows to transfer any locked token from this contract.
* Only admin can call this method.
* While it is not allowed to claim previously enabled or paused tokens,
* the admin should still verify that the claimed token is a valid ERC20 token contract.
* @param _token address of the token, if it is not provided (0x00..00), native coins will be transferred.
* @param _to address that will receive the locked tokens on this contract.
*/
function claimTokens(address _token, address _to) external onlyAdmin {
require(tokenStatus[_token] == TokenStatus.DISABLED, "SBCWrapper: token already swappable");
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider to add a comment that the admin needs to make sure that the claimable token does not have the same storage with a token configured for the swap (the situation when different tokens addresses refers to the same storage contract).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure.


_claimValues(_token, _to);
}

function _swapTokens(
address _receiver,
address _token,
uint256 _amount
) internal {
uint256 acquired = (_amount * tokenRate[_token]) / 1 ether;
require(acquired > 0, "SBCWrapper: invalid amount");

sbcToken.mint(_receiver, acquired);

emit Swap(_token, _receiver, _amount, acquired);
}
}
17 changes: 17 additions & 0 deletions contracts/SBCWrapperProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: CC0-1.0

pragma solidity 0.8.7;

import "./utils/EIP1967Proxy.sol";
import "./SBCWrapper.sol";

/**
* @title SBCWrapperProxy
* @dev Upgradeable version of the underlying SBCWrapper.
*/
contract SBCWrapperProxy is EIP1967Proxy {
constructor(address _admin, SBCToken _token) {
_setAdmin(_admin);
_setImplementation(address(new SBCWrapper(_token)));
}
}
13 changes: 0 additions & 13 deletions contracts/interfaces/IERC165.sol

This file was deleted.

Loading