Skip to content

Instantly share code, notes, and snippets.

@mischat
Created May 27, 2020 23:33
Show Gist options
  • Save mischat/89f6c715490114993d400308981971c3 to your computer and use it in GitHub Desktop.
Save mischat/89f6c715490114993d400308981971c3 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.5.17+commit.d19bba13.js&optimize=true&gist=
/**
* Controller - The Consumer Contract Wallet
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./internals/ownable.sol";
import "./internals/transferrable.sol";
/// @title The IController interface provides access to the isController and isAdmin checks.
interface IController {
function isController(address) external view returns (bool);
function isAdmin(address) external view returns (bool);
}
/// @title Controller stores a list of controller addresses that can be used for authentication in other contracts.
/// @notice The Controller implements a hierarchy of concepts, Owner, Admin, and the Controllers.
/// @dev Owner can change the Admins
/// @dev Admins and can the Controllers
/// @dev Controllers are used by the application.
contract Controller is IController, Ownable, Transferrable {
event AddedController(address _sender, address _controller);
event RemovedController(address _sender, address _controller);
event AddedAdmin(address _sender, address _admin);
event RemovedAdmin(address _sender, address _admin);
event Claimed(address _to, address _asset, uint256 _amount);
event Stopped(address _sender);
event Started(address _sender);
mapping(address => bool) private _isAdmin;
uint256 private _adminCount;
mapping(address => bool) private _isController;
uint256 private _controllerCount;
bool private _stopped;
/// @notice Constructor initializes the owner with the provided address.
/// @param _ownerAddress_ address of the owner.
constructor(address payable _ownerAddress_) public Ownable(_ownerAddress_, false) {}
/// @notice Checks if message sender is an admin.
modifier onlyAdmin() {
require(_isAdmin[msg.sender], "sender is not an admin");
_;
}
/// @notice Check if Owner or Admin
modifier onlyAdminOrOwner() {
require(_isOwner(msg.sender) || _isAdmin[msg.sender], "sender is not an admin");
_;
}
/// @notice Check if controller is stopped
modifier notStopped() {
require(!isStopped(), "controller is stopped");
_;
}
/// @notice Add a new admin to the list of admins.
/// @param _account address to add to the list of admins.
function addAdmin(address _account) external onlyOwner notStopped {
_addAdmin(_account);
}
/// @notice Remove a admin from the list of admins.
/// @param _account address to remove from the list of admins.
function removeAdmin(address _account) external onlyOwner {
_removeAdmin(_account);
}
/// @return the current number of admins.
function adminCount() external view returns (uint256) {
return _adminCount;
}
/// @notice Add a new controller to the list of controllers.
/// @param _account address to add to the list of controllers.
function addController(address _account) external onlyAdminOrOwner notStopped {
_addController(_account);
}
/// @notice Remove a controller from the list of controllers.
/// @param _account address to remove from the list of controllers.
function removeController(address _account) external onlyAdminOrOwner {
_removeController(_account);
}
/// @notice count the Controllers
/// @return the current number of controllers.
function controllerCount() external view returns (uint256) {
return _controllerCount;
}
/// @notice is an address an Admin?
/// @return true if the provided account is an admin.
function isAdmin(address _account) external view notStopped returns (bool) {
return _isAdmin[_account];
}
/// @notice is an address a Controller?
/// @return true if the provided account is a controller.
function isController(address _account) external view notStopped returns (bool) {
return _isController[_account];
}
/// @notice this function can be used to see if the controller has been stopped
/// @return true is the Controller has been stopped
function isStopped() public view returns (bool) {
return _stopped;
}
/// @notice Internal-only function that adds a new admin.
function _addAdmin(address _account) private {
require(!_isAdmin[_account], "provided account is already an admin");
require(!_isController[_account], "provided account is already a controller");
require(!_isOwner(_account), "provided account is already the owner");
require(_account != address(0), "provided account is the zero address");
_isAdmin[_account] = true;
_adminCount++;
emit AddedAdmin(msg.sender, _account);
}
/// @notice Internal-only function that removes an existing admin.
function _removeAdmin(address _account) private {
require(_isAdmin[_account], "provided account is not an admin");
_isAdmin[_account] = false;
_adminCount--;
emit RemovedAdmin(msg.sender, _account);
}
/// @notice Internal-only function that adds a new controller.
function _addController(address _account) private {
require(!_isAdmin[_account], "provided account is already an admin");
require(!_isController[_account], "provided account is already a controller");
require(!_isOwner(_account), "provided account is already the owner");
require(_account != address(0), "provided account is the zero address");
_isController[_account] = true;
_controllerCount++;
emit AddedController(msg.sender, _account);
}
/// @notice Internal-only function that removes an existing controller.
function _removeController(address _account) private {
require(_isController[_account], "provided account is not a controller");
_isController[_account] = false;
_controllerCount--;
emit RemovedController(msg.sender, _account);
}
/// @notice stop our controllers and admins from being useable
function stop() external onlyAdminOrOwner {
_stopped = true;
emit Stopped(msg.sender);
}
/// @notice start our controller again
function start() external onlyOwner {
_stopped = false;
emit Started(msg.sender);
}
//// @notice Withdraw tokens from the smart contract to the specified account.
function claim(address payable _to, address _asset, uint256 _amount) external onlyAdmin notStopped {
_safeTransfer(_to, _asset, _amount);
emit Claimed(_to, _asset, _amount);
}
}
/**
* Holder (aka Asset Contract) - The Consumer Contract Wallet
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./externals/ERC20.sol";
import "./externals/SafeMath.sol";
import "./internals/transferrable.sol";
import "./internals/balanceable.sol";
import "./internals/burner.sol";
import "./internals/controllable.sol";
import "./internals/tokenWhitelistable.sol";
/// @title Holder - The TKN Asset Contract
/// @notice When the TKN contract calls the burn method, a share of the tokens held by this contract are disbursed to the burner.
contract Holder is Balanceable, ENSResolvable, Controllable, Transferrable, TokenWhitelistable {
using SafeMath for uint256;
event Received(address _from, uint256 _amount);
event CashAndBurned(address _to, address _asset, uint256 _amount);
event Claimed(address _to, address _asset, uint256 _amount);
/// @dev Check if the sender is the burner contract
modifier onlyBurner() {
require(msg.sender == _burner, "burner contract is not the sender");
_;
}
// Burner token which can be burned to redeem shares.
address private _burner;
/// @notice Constructor initializes the holder contract.
/// @param _burnerContract_ is the address of the token contract TKN with burning functionality.
/// @param _ens_ is the address of the ENS registry.
/// @param _tokenWhitelistNode_ is the ENS node of the Token whitelist.
/// @param _controllerNode_ is the ENS node of the Controller
constructor(address _burnerContract_, address _ens_, bytes32 _tokenWhitelistNode_, bytes32 _controllerNode_)
public
ENSResolvable(_ens_)
Controllable(_controllerNode_)
TokenWhitelistable(_tokenWhitelistNode_)
{
_burner = _burnerContract_;
}
/// @notice Ether may be sent from anywhere.
function() external payable {
emit Received(msg.sender, msg.value);
}
/// @notice Burn handles disbursing a share of tokens in this contract to a given address.
/// @param _to The address to disburse to
/// @param _amount The amount of TKN that will be burned if this succeeds
function burn(address payable _to, uint256 _amount) external onlyBurner returns (bool) {
if (_amount == 0) {
return true;
}
// The burner token deducts from the supply before calling.
uint256 supply = IBurner(_burner).currentSupply().add(_amount);
address[] memory redeemableAddresses = _redeemableTokens();
for (uint256 i = 0; i < redeemableAddresses.length; i++) {
uint256 redeemableBalance = _balance(address(this), redeemableAddresses[i]);
if (redeemableBalance > 0) {
uint256 redeemableAmount = redeemableBalance.mul(_amount).div(supply);
_safeTransfer(_to, redeemableAddresses[i], redeemableAmount);
emit CashAndBurned(_to, redeemableAddresses[i], redeemableAmount);
}
}
return true;
}
/// @notice This allows for the admin to reclaim the non-redeemableTokens.
/// @param _to this is the address which the reclaimed tokens will be sent to.
/// @param _nonRedeemableAddresses this is the array of tokens to be claimed.
function nonRedeemableTokenClaim(address payable _to, address[] calldata _nonRedeemableAddresses) external onlyAdmin returns (bool) {
for (uint256 i = 0; i < _nonRedeemableAddresses.length; i++) {
//revert if token is redeemable
require(!_isTokenRedeemable(_nonRedeemableAddresses[i]), "redeemables cannot be claimed");
uint256 claimBalance = _balance(address(this), _nonRedeemableAddresses[i]);
if (claimBalance > 0) {
_safeTransfer(_to, _nonRedeemableAddresses[i], claimBalance);
emit Claimed(_to, _nonRedeemableAddresses[i], claimBalance);
}
}
return true;
}
/// @notice Returned the address of the burner contract.
/// @return the TKN address.
function burner() external view returns (address) {
return _burner;
}
}
/**
* Licence - The Consumer Contract Wallet
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./externals/SafeMath.sol";
import "./externals/SafeERC20.sol";
import "./internals/controllable.sol";
import "./internals/ensResolvable.sol";
import "./internals/transferrable.sol";
/// @title ILicence interface describes methods for loading a TokenCard and updating licence amount.
interface ILicence {
function load(address, uint256) external payable;
function updateLicenceAmount(uint256) external;
}
/// @title Licence loads the TokenCard and transfers the licence amout to the TKN Holder Contract.
/// @notice the rest of the amount gets sent to the CryptoFloat
contract Licence is Transferrable, ENSResolvable, Controllable {
using SafeMath for uint256;
using SafeERC20 for ERC20;
/*******************/
/* Events */
/*****************/
event UpdatedLicenceDAO(address _newDAO);
event UpdatedCryptoFloat(address _newFloat);
event UpdatedTokenHolder(address _newHolder);
event UpdatedTKNContractAddress(address _newTKN);
event UpdatedLicenceAmount(uint256 _newAmount);
event TransferredToTokenHolder(address _from, address _to, address _asset, uint256 _amount);
event TransferredToCryptoFloat(address _from, address _to, address _asset, uint256 _amount);
event Claimed(address _to, address _asset, uint256 _amount);
/// @notice This is 100% scaled up by a factor of 10 to give us an extra 1 decimal place of precision
uint256 public constant MAX_AMOUNT_SCALE = 1000;
uint256 public constant MIN_AMOUNT_SCALE = 1;
address private _tknContractAddress = 0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a; // solium-disable-line uppercase
address payable private _cryptoFloat;
address payable private _tokenHolder;
address private _licenceDAO;
bool private _lockedCryptoFloat;
bool private _lockedTokenHolder;
bool private _lockedLicenceDAO;
bool private _lockedTKNContractAddress;
/// @notice This is the _licenceAmountScaled by a factor of 10
/// @dev i.e. 1% is 10 _licenceAmountScaled, 0.1% is 1 _licenceAmountScaled
uint256 private _licenceAmountScaled;
/// @notice Reverts if called by any address other than the DAO contract.
modifier onlyDAO() {
require(msg.sender == _licenceDAO, "the sender isn't the DAO");
_;
}
/// @notice Constructor initializes the card licence contract.
/// @param _licence_ is the initial card licence amount. this number is scaled 10 = 1%, 9 = 0.9%
/// @param _float_ is the address of the multi-sig cryptocurrency float contract.
/// @param _holder_ is the address of the token holder contract
/// @param _tknAddress_ is the address of the TKN ERC20 contract
/// @param _ens_ is the address of the ENS Registry
/// @param _controllerNode_ is the ENS node corresponding to the controller
constructor(uint256 _licence_, address payable _float_, address payable _holder_, address _tknAddress_, address _ens_, bytes32 _controllerNode_)
public
ENSResolvable(_ens_)
Controllable(_controllerNode_)
{
require(MIN_AMOUNT_SCALE <= _licence_ && _licence_ <= MAX_AMOUNT_SCALE, "licence amount out of range");
_licenceAmountScaled = _licence_;
_cryptoFloat = _float_;
_tokenHolder = _holder_;
if (_tknAddress_ != address(0)) {
_tknContractAddress = _tknAddress_;
}
}
/// @notice Ether can be deposited from any source, so this contract should be payable by anyone.
function() external payable {}
/// @notice this allows for people to see the scaled licence amount
/// @return the scaled licence amount, used to calculate the split when loading.
function licenceAmountScaled() external view returns (uint256) {
return _licenceAmountScaled;
}
/// @notice allows one to see the address of the CryptoFloat
/// @return the address of the multi-sig cryptocurrency float contract.
function cryptoFloat() external view returns (address) {
return _cryptoFloat;
}
/// @notice allows one to see the address TKN holder contract
/// @return the address of the token holder contract.
function tokenHolder() external view returns (address) {
return _tokenHolder;
}
/// @notice allows one to see the address of the DAO
/// @return the address of the DAO contract.
function licenceDAO() external view returns (address) {
return _licenceDAO;
}
/// @notice The address of the TKN token
/// @return the address of the TKN contract.
function tknContractAddress() external view returns (address) {
return _tknContractAddress;
}
/// @notice This locks the cryptoFloat address
/// @dev so that it can no longer be updated
function lockFloat() external onlyAdmin {
_lockedCryptoFloat = true;
}
/// @notice This locks the TokenHolder address
/// @dev so that it can no longer be updated
function lockHolder() external onlyAdmin {
_lockedTokenHolder = true;
}
/// @notice This locks the DAO address
/// @dev so that it can no longer be updated
function lockLicenceDAO() external onlyAdmin {
_lockedLicenceDAO = true;
}
/// @notice This locks the TKN address
/// @dev so that it can no longer be updated
function lockTKNContractAddress() external onlyAdmin {
_lockedTKNContractAddress = true;
}
/// @notice Updates the address of the cyptoFloat.
/// @param _newFloat This is the new address for the CryptoFloat
function updateFloat(address payable _newFloat) external onlyAdmin {
require(!floatLocked(), "float is locked");
_cryptoFloat = _newFloat;
emit UpdatedCryptoFloat(_newFloat);
}
/// @notice Updates the address of the Holder contract.
/// @param _newHolder This is the new address for the TokenHolder
function updateHolder(address payable _newHolder) external onlyAdmin {
require(!holderLocked(), "holder contract is locked");
_tokenHolder = _newHolder;
emit UpdatedTokenHolder(_newHolder);
}
/// @notice Updates the address of the DAO contract.
/// @param _newDAO This is the new address for the Licence DAO
function updateLicenceDAO(address _newDAO) external onlyAdmin {
require(!licenceDAOLocked(), "DAO is locked");
_licenceDAO = _newDAO;
emit UpdatedLicenceDAO(_newDAO);
}
/// @notice Updates the address of the TKN contract.
/// @param _newTKN This is the new address for the TKN contract
function updateTKNContractAddress(address _newTKN) external onlyAdmin {
require(!tknContractAddressLocked(), "TKN is locked");
_tknContractAddress = _newTKN;
emit UpdatedTKNContractAddress(_newTKN);
}
/// @notice Updates the TKN licence amount
/// @param _newAmount is a number between MIN_AMOUNT_SCALE (1) and MAX_AMOUNT_SCALE
function updateLicenceAmount(uint256 _newAmount) external onlyDAO {
require(MIN_AMOUNT_SCALE <= _newAmount && _newAmount <= MAX_AMOUNT_SCALE, "licence amount out of range");
_licenceAmountScaled = _newAmount;
emit UpdatedLicenceAmount(_newAmount);
}
/// @notice Load the holder and float contracts based on the licence amount and asset amount.
/// @param _asset is the address of an ERC20 token or 0x0 for ether.
/// @param _amount is the amount of assets to be transferred including the licence amount.
function load(address _asset, uint256 _amount) external payable {
uint256 loadAmount = _amount;
// If TKN then no licence to be paid
if (_asset == _tknContractAddress) {
ERC20(_asset).safeTransferFrom(msg.sender, _cryptoFloat, loadAmount);
} else {
loadAmount = _amount.mul(MAX_AMOUNT_SCALE).div(_licenceAmountScaled + MAX_AMOUNT_SCALE);
uint256 licenceAmount = _amount.sub(loadAmount);
if (_asset != address(0)) {
ERC20(_asset).safeTransferFrom(msg.sender, _tokenHolder, licenceAmount);
ERC20(_asset).safeTransferFrom(msg.sender, _cryptoFloat, loadAmount);
} else {
require(msg.value == _amount, "ETH sent is not equal to amount");
_tokenHolder.transfer(licenceAmount);
_cryptoFloat.transfer(loadAmount);
}
emit TransferredToTokenHolder(msg.sender, _tokenHolder, _asset, licenceAmount);
}
emit TransferredToCryptoFloat(msg.sender, _cryptoFloat, _asset, loadAmount);
}
//// @notice Withdraw tokens from the smart contract to the specified account.
function claim(address payable _to, address _asset, uint256 _amount) external onlyAdmin {
_safeTransfer(_to, _asset, _amount);
emit Claimed(_to, _asset, _amount);
}
/// @notice returns whether or not the CryptoFloat address is locked
function floatLocked() public view returns (bool) {
return _lockedCryptoFloat;
}
/// @notice returns whether or not the TokenHolder address is locked
function holderLocked() public view returns (bool) {
return _lockedTokenHolder;
}
/// @notice returns whether or not the Licence DAO address is locked
function licenceDAOLocked() public view returns (bool) {
return _lockedLicenceDAO;
}
/// @notice returns whether or not the TKN address is locked
function tknContractAddressLocked() public view returns (bool) {
return _lockedTKNContractAddress;
}
}
/**
* Oracle - The Consumer Contract Wallet
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./internals/controllable.sol";
import "./internals/transferrable.sol";
import "./internals/ensResolvable.sol";
import "./internals/date.sol";
import "./internals/parseIntScientific.sol";
import "./internals/tokenWhitelistable.sol";
import "./externals/SafeMath.sol";
import "./externals/oraclizeAPI_0.5.sol";
import "./externals/base64.sol";
/// @title Oracle provides asset exchange rates and conversion functionality.
contract Oracle is ENSResolvable, usingOraclize, Transferrable, Base64, Date, Controllable, ParseIntScientific, TokenWhitelistable {
using strings for *;
using SafeMath for uint256;
/*******************/
/* Events */
/*****************/
event SetGasPrice(address _sender, uint256 _gasPrice);
event RequestedUpdate(string _symbol, bytes32 _queryID);
event FailedUpdateRequest(string _reason);
event VerifiedProof(bytes _publicKey, string _result);
event SetCryptoComparePublicKey(address _sender, bytes _publicKey);
event Claimed(address _to, address _asset, uint256 _amount);
/**********************/
/* Constants */
/********************/
uint256 private constant _PROOF_LEN = 165;
uint256 private constant _ECDSA_SIG_LEN = 65;
uint256 private constant _ENCODING_BYTES = 2;
uint256 private constant _HEADERS_LEN = _PROOF_LEN - 2 * _ENCODING_BYTES - _ECDSA_SIG_LEN; // 2 bytes encoding headers length + 2 for signature.
uint256 private constant _DIGEST_BASE64_LEN = 44; //base64 encoding of the SHA256 hash (32-bytes) of the result: fixed length.
uint256 private constant _DIGEST_OFFSET = _HEADERS_LEN - _DIGEST_BASE64_LEN; // the starting position of the result hash in the headers string.
uint256 private constant _MAX_BYTE_SIZE = 256; //for calculating length encoding
// This is how the cryptocompare json begins
bytes32 private constant _PREFIX_HASH = keccak256('{"ETH":');
bytes public cryptoCompareAPIPublicKey;
mapping(bytes32 => address) private _queryToToken;
/// @notice Construct the oracle with multiple controllers, address resolver and custom gas price.
/// @param _resolver_ is the address of the oraclize resolver
/// @param _ens_ is the address of the ENS.
/// @param _controllerNode_ is the ENS node corresponding to the Controller.
/// @param _tokenWhitelistNode_ is the ENS corresponding to the Token Whitelist.
constructor(address _resolver_, address _ens_, bytes32 _controllerNode_, bytes32 _tokenWhitelistNode_)
public
ENSResolvable(_ens_)
Controllable(_controllerNode_)
TokenWhitelistable(_tokenWhitelistNode_)
{
cryptoCompareAPIPublicKey = hex"a0f4f688350018ad1b9785991c0bde5f704b005dc79972b114dbed4a615a983710bfc647ebe5a320daa28771dce6a2d104f5efa2e4a85ba3760b76d46f8571ca";
OAR = OraclizeAddrResolverI(_resolver_);
oraclize_setCustomGasPrice(10000000000);
oraclize_setProof(proofType_Native);
}
/// @notice Updates the Crypto Compare public API key.
/// @param _publicKey new Crypto Compare public API key
function updateCryptoCompareAPIPublicKey(bytes calldata _publicKey) external onlyAdmin {
cryptoCompareAPIPublicKey = _publicKey;
emit SetCryptoComparePublicKey(msg.sender, _publicKey);
}
/// @notice Sets the gas price used by Oraclize query.
/// @param _gasPrice in wei for Oraclize
function setCustomGasPrice(uint256 _gasPrice) external onlyController {
oraclize_setCustomGasPrice(_gasPrice);
emit SetGasPrice(msg.sender, _gasPrice);
}
/// @notice Update ERC20 token exchange rates for all supported tokens.
/// @param _gasLimit the gas limit is passed, this is used for the Oraclize callback
function updateTokenRates(uint256 _gasLimit) external payable onlyController {
_updateTokenRates(_gasLimit);
}
/// @notice Update ERC20 token exchange rates for the list of tokens provided.
/// @param _gasLimit the gas limit is passed, this is used for the Oraclize callback
/// @param _tokenList the list of tokens that need to be updated
function updateTokenRatesList(uint256 _gasLimit, address[] calldata _tokenList) external payable onlyController {
_updateTokenRatesList(_gasLimit, _tokenList);
}
/// @notice Withdraw tokens from the smart contract to the specified account.
function claim(address payable _to, address _asset, uint256 _amount) external onlyAdmin {
_safeTransfer(_to, _asset, _amount);
emit Claimed(_to, _asset, _amount);
}
/// @notice Handle Oraclize query callback and verifiy the provided origin proof.
/// @param _queryID Oraclize query ID.
/// @param _result query result in JSON format.
/// @param _proof origin proof from crypto compare.
// solium-disable-next-line mixedcase
function __callback(bytes32 _queryID, string memory _result, bytes memory _proof) public {
// Require that the caller is the Oraclize contract.
require(msg.sender == oraclize_cbAddress(), "sender is not oraclize");
// Use the query ID to find the matching token address.
address token = _queryToToken[_queryID];
// Get the corresponding token object.
(, , , bool available, , , uint256 lastUpdate) = _getTokenInfo(token);
require(available, "token must be available");
bool valid;
uint256 timestamp;
(valid, timestamp) = _verifyProof(_result, _proof, cryptoCompareAPIPublicKey, lastUpdate);
// Require that the proof is valid.
if (valid) {
// Parse the JSON result to get the rate in wei.
uint256 parsedRate = _parseIntScientificWei(parseRate(_result));
// Set the update time of the token rate.
uint256 parsedLastUpdate = timestamp;
// Remove query from the list.
delete _queryToToken[_queryID];
_updateTokenRate(token, parsedRate, parsedLastUpdate);
}
}
/// @notice Extracts JSON rate value from the response object.
/// @param _json body of the JSON response from the CryptoCompare API.
function parseRate(string memory _json) internal pure returns (string memory) {
uint256 jsonLen = abi.encodePacked(_json).length;
//{"ETH":}.length = 8, assuming a (maximum of) 18 digit prevision
require(jsonLen > 8 && jsonLen <= 28, "misformatted input");
bytes memory jsonPrefix = new bytes(7);
copyBytes(abi.encodePacked(_json), 0, 7, jsonPrefix, 0);
require(keccak256(jsonPrefix) == _PREFIX_HASH, "prefix mismatch");
strings.slice memory body = _json.toSlice();
body.split(":".toSlice());
//we are sure that ':' is included in the string, body now contains the rate+'}'
jsonLen = body._len;
body.until("}".toSlice());
require(body._len == jsonLen - 1, "not json format");
//ensure that the json is properly terminated with a '}'
return body.toString();
}
/// @notice Re-usable helper function that performs the Oraclize Query.
/// @param _gasLimit the gas limit is passed, this is used for the Oraclize callback
function _updateTokenRates(uint256 _gasLimit) private {
address[] memory tokenAddresses = _tokenAddressArray();
// Check if there are any existing tokens.
if (tokenAddresses.length == 0) {
// Emit a query failure event.
emit FailedUpdateRequest("no tokens");
// Check if the contract has enough Ether to pay for the query.
} else if (oraclize_getPrice("URL") * tokenAddresses.length > address(this).balance) {
// Emit a query failure event.
emit FailedUpdateRequest("insufficient balance");
} else {
// Set up the cryptocompare API query strings.
strings.slice memory apiPrefix = "https://min-api.cryptocompare.com/data/price?fsym=".toSlice();
strings.slice memory apiSuffix = "&tsyms=ETH&sign=true".toSlice();
// Create a new oraclize query for each supported token.
for (uint256 i = 0; i < tokenAddresses.length; i++) {
// Store the token symbol used in the query.
(string memory symbol, , , , , , ) = _getTokenInfo(tokenAddresses[i]);
strings.slice memory sym = symbol.toSlice();
// Create a new oraclize query from the component strings.
bytes32 queryID = oraclize_query("URL", apiPrefix.concat(sym).toSlice().concat(apiSuffix), _gasLimit);
// Store the query ID together with the associated token address.
_queryToToken[queryID] = tokenAddresses[i];
// Emit the query success event.
emit RequestedUpdate(sym.toString(), queryID);
}
}
}
/// @notice Re-usable helper function that performs the Oraclize Query for a specific list of tokens.
/// @param _gasLimit the gas limit is passed, this is used for the Oraclize callback.
/// @param _tokenList the list of tokens that need to be updated.
function _updateTokenRatesList(uint256 _gasLimit, address[] memory _tokenList) private {
// Check if there are any existing tokens.
if (_tokenList.length == 0) {
// Emit a query failure event.
emit FailedUpdateRequest("empty token list");
// Check if the contract has enough Ether to pay for the query.
} else if (oraclize_getPrice("URL") * _tokenList.length > address(this).balance) {
// Emit a query failure event.
emit FailedUpdateRequest("insufficient balance");
} else {
// Set up the cryptocompare API query strings.
strings.slice memory apiPrefix = "https://min-api.cryptocompare.com/data/price?fsym=".toSlice();
strings.slice memory apiSuffix = "&tsyms=ETH&sign=true".toSlice();
// Create a new oraclize query for each supported token.
for (uint256 i = 0; i < _tokenList.length; i++) {
//token must exist, revert if it doesn't
(string memory tokenSymbol, , , bool available, , , ) = _getTokenInfo(_tokenList[i]);
require(available, "token must be available");
// Store the token symbol used in the query.
strings.slice memory symbol = tokenSymbol.toSlice();
// Create a new oraclize query from the component strings.
bytes32 queryID = oraclize_query("URL", apiPrefix.concat(symbol).toSlice().concat(apiSuffix), _gasLimit);
// Store the query ID together with the associated token address.
_queryToToken[queryID] = _tokenList[i];
// Emit the query success event.
emit RequestedUpdate(symbol.toString(), queryID);
}
}
}
/// @notice Verify the origin proof returned by the cryptocompare API.
/// @param _result query result in JSON format.
/// @param _proof origin proof from cryptocompare.
/// @param _publicKey cryptocompare public key.
/// @param _lastUpdate timestamp of the last time the requested token was updated.
function _verifyProof(string memory _result, bytes memory _proof, bytes memory _publicKey, uint256 _lastUpdate) private returns (bool, uint256) {
// expecting fixed length proofs
if (_proof.length != _PROOF_LEN) {
revert("invalid proof length");
}
// proof should be 65 bytes long: R (32 bytes) + S (32 bytes) + v (1 byte)
if (uint256(uint8(_proof[1])) != _ECDSA_SIG_LEN) {
revert("invalid signature length");
}
bytes memory signature = new bytes(_ECDSA_SIG_LEN);
signature = copyBytes(_proof, 2, _ECDSA_SIG_LEN, signature, 0);
// Extract the headers, big endian encoding of headers length
if (
uint256(uint8(_proof[_ENCODING_BYTES + _ECDSA_SIG_LEN])) * _MAX_BYTE_SIZE + uint256(uint8(_proof[_ENCODING_BYTES + _ECDSA_SIG_LEN + 1])) !=
_HEADERS_LEN
) {
revert("invalid headers length");
}
bytes memory headers = new bytes(_HEADERS_LEN);
headers = copyBytes(_proof, 2 * _ENCODING_BYTES + _ECDSA_SIG_LEN, _HEADERS_LEN, headers, 0);
// Check if the signature is valid and if the signer address is matching.
if (!_verifySignature(headers, signature, _publicKey)) {
revert("invalid signature");
}
// Check if the date is valid.
bytes memory dateHeader = new bytes(20);
// keep only the relevant string(e.g. "16 Nov 2018 16:22:18")
dateHeader = copyBytes(headers, 11, 20, dateHeader, 0);
bool dateValid;
uint256 timestamp;
(dateValid, timestamp) = _verifyDate(string(dateHeader), _lastUpdate);
// Check whether the date returned is valid or not
if (!dateValid) {
revert("invalid date");
}
// Check if the signed digest hash matches the result hash.
bytes memory digest = new bytes(_DIGEST_BASE64_LEN);
digest = copyBytes(headers, _DIGEST_OFFSET, _DIGEST_BASE64_LEN, digest, 0);
if (keccak256(abi.encodePacked(sha256(abi.encodePacked(_result)))) != keccak256(_base64decode(digest))) {
revert("result hash not matching");
}
emit VerifiedProof(_publicKey, _result);
return (true, timestamp);
}
/// @notice Verify the HTTP headers and the signature
/// @param _headers HTTP headers provided by the cryptocompare api
/// @param _signature signature provided by the cryptocompare api
/// @param _publicKey cryptocompare public key.
function _verifySignature(bytes memory _headers, bytes memory _signature, bytes memory _publicKey) private returns (bool) {
address signer;
bool signatureOK;
// Checks if the signature is valid by hashing the headers
(signatureOK, signer) = ecrecovery(sha256(_headers), _signature);
return signatureOK && signer == address(uint160(uint256(keccak256(_publicKey))));
}
/// @notice Verify the signed HTTP date header.
/// @param _dateHeader extracted date string e.g. Wed, 12 Sep 2018 15:18:14 GMT.
/// @param _lastUpdate timestamp of the last time the requested token was updated.
function _verifyDate(string memory _dateHeader, uint256 _lastUpdate) private pure returns (bool, uint256) {
// called by verifyProof(), _dateHeader is always a string of length = 20
assert(abi.encodePacked(_dateHeader).length == 20);
// Split the date string and get individual date components.
strings.slice memory date = _dateHeader.toSlice();
strings.slice memory timeDelimiter = ":".toSlice();
strings.slice memory dateDelimiter = " ".toSlice();
uint256 day = _parseIntScientific(date.split(dateDelimiter).toString());
require(day > 0 && day < 32, "day error");
uint256 month = _monthToNumber(date.split(dateDelimiter).toString());
require(month > 0 && month < 13, "month error");
uint256 year = _parseIntScientific(date.split(dateDelimiter).toString());
require(year > 2017 && year < 3000, "year error");
uint256 hour = _parseIntScientific(date.split(timeDelimiter).toString());
require(hour < 25, "hour error");
uint256 minute = _parseIntScientific(date.split(timeDelimiter).toString());
require(minute < 60, "minute error");
uint256 second = _parseIntScientific(date.split(timeDelimiter).toString());
require(second < 60, "second error");
uint256 timestamp = year * (10**10) + month * (10**8) + day * (10**6) + hour * (10**4) + minute * (10**2) + second;
return (timestamp > _lastUpdate, timestamp);
}
}
/**
* TokenWhitelist - The Consumer Contract Wallet
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./internals/controllable.sol";
import "./internals/transferrable.sol";
import "./internals/bytesUtils.sol";
import "./externals/strings.sol";
import "./externals/SafeMath.sol";
/// @title The ITokenWhitelist interface provides access to a whitelist of tokens.
interface ITokenWhitelist {
function getTokenInfo(address) external view returns (string memory, uint256, uint256, bool, bool, bool, uint256);
function getStablecoinInfo() external view returns (string memory, uint256, uint256, bool, bool, bool, uint256);
function tokenAddressArray() external view returns (address[] memory);
function redeemableTokens() external view returns (address[] memory);
function methodIdWhitelist(bytes4) external view returns (bool);
function getERC20RecipientAndAmount(address, bytes calldata) external view returns (address, uint256);
function stablecoin() external view returns (address);
function updateTokenRate(address, uint256, uint256) external;
}
/// @title TokenWhitelist stores a list of tokens used by the Consumer Contract Wallet, the Oracle, the TKN Holder and the TKN Licence Contract
contract TokenWhitelist is ENSResolvable, Controllable, Transferrable {
using strings for *;
using SafeMath for uint256;
using BytesUtils for bytes;
event UpdatedTokenRate(address _sender, address _token, uint256 _rate);
event UpdatedTokenLoadable(address _sender, address _token, bool _loadable);
event UpdatedTokenRedeemable(address _sender, address _token, bool _redeemable);
event AddedToken(address _sender, address _token, string _symbol, uint256 _magnitude, bool _loadable, bool _redeemable);
event RemovedToken(address _sender, address _token);
event AddedMethodId(bytes4 _methodId);
event RemovedMethodId(bytes4 _methodId);
event AddedExclusiveMethod(address _token, bytes4 _methodId);
event RemovedExclusiveMethod(address _token, bytes4 _methodId);
event Claimed(address _to, address _asset, uint256 _amount);
/// @dev these are the methods whitelisted by default in executeTransaction() for protected tokens
bytes4 private constant _APPROVE = 0x095ea7b3; // keccak256(approve(address,uint256)) => 0x095ea7b3
bytes4 private constant _BURN = 0x42966c68; // keccak256(burn(uint256)) => 0x42966c68
bytes4 private constant _TRANSFER = 0xa9059cbb; // keccak256(transfer(address,uint256)) => 0xa9059cbb
bytes4 private constant _TRANSFER_FROM = 0x23b872dd; // keccak256(transferFrom(address,address,uint256)) => 0x23b872dd
struct Token {
string symbol; // Token symbol
uint256 magnitude; // 10^decimals
uint256 rate; // Token exchange rate in wei
bool available; // Flags if the token is available or not
bool loadable; // Flags if token is loadable to the TokenCard
bool redeemable; // Flags if token is redeemable in the TKN Holder contract
uint256 lastUpdate; // Time of the last rate update
}
mapping(address => Token) private _tokenInfoMap;
// @notice specifies whitelisted methodIds for protected tokens in wallet's excuteTranaction() e.g. keccak256(transfer(address,uint256)) => 0xa9059cbb
mapping(bytes4 => bool) private _methodIdWhitelist;
address[] private _tokenAddressArray;
/// @notice keeping track of how many redeemable tokens are in the tokenWhitelist
uint256 private _redeemableCounter;
/// @notice Address of the stablecoin.
address private _stablecoin;
/// @notice is registered ENS node identifying the oracle contract.
bytes32 private _oracleNode;
/// @notice Constructor initializes ENSResolvable, and Controllable.
/// @param _ens_ is the ENS registry address.
/// @param _oracleNode_ is the ENS node of the Oracle.
/// @param _controllerNode_ is our Controllers node.
/// @param _stablecoinAddress_ is the address of the stablecoint used by the wallet for the card load limit.
constructor(address _ens_, bytes32 _oracleNode_, bytes32 _controllerNode_, address _stablecoinAddress_)
public
ENSResolvable(_ens_)
Controllable(_controllerNode_)
{
_oracleNode = _oracleNode_;
_stablecoin = _stablecoinAddress_;
//a priori ERC20 whitelisted methods
_methodIdWhitelist[_APPROVE] = true;
_methodIdWhitelist[_BURN] = true;
_methodIdWhitelist[_TRANSFER] = true;
_methodIdWhitelist[_TRANSFER_FROM] = true;
}
modifier onlyAdminOrOracle() {
address oracleAddress = _ensResolve(_oracleNode);
require(_isAdmin(msg.sender) || msg.sender == oracleAddress, "either oracle or admin");
_;
}
/// @notice Add ERC20 tokens to the list of whitelisted tokens.
/// @param _tokens ERC20 token contract addresses.
/// @param _symbols ERC20 token names.
/// @param _magnitude 10 to the power of number of decimal places used by each ERC20 token.
/// @param _loadable is a bool that states whether or not a token is loadable to the TokenCard.
/// @param _redeemable is a bool that states whether or not a token is redeemable in the TKN Holder Contract.
/// @param _lastUpdate is a unit representing an ISO datetime e.g. 20180913153211.
function addTokens(
address[] calldata _tokens,
bytes32[] calldata _symbols,
uint256[] calldata _magnitude,
bool[] calldata _loadable,
bool[] calldata _redeemable,
uint256 _lastUpdate
) external onlyAdmin {
// Require that all parameters have the same length.
require(
_tokens.length == _symbols.length &&
_tokens.length == _magnitude.length &&
_tokens.length == _loadable.length &&
_tokens.length == _loadable.length,
"parameter lengths do not match"
);
// Add each token to the list of supported tokens.
for (uint256 i = 0; i < _tokens.length; i++) {
// Require that the token isn't already available.
require(!_tokenInfoMap[_tokens[i]].available, "token already available");
// Store the intermediate values.
string memory symbol = _symbols[i].toSliceB32().toString();
// Add the token to the token list.
_tokenInfoMap[_tokens[i]] = Token({
symbol: symbol,
magnitude: _magnitude[i],
rate: 0,
available: true,
loadable: _loadable[i],
redeemable: _redeemable[i],
lastUpdate: _lastUpdate
});
// Add the token address to the address list.
_tokenAddressArray.push(_tokens[i]);
//if the token is redeemable increase the redeemableCounter
if (_redeemable[i]) {
_redeemableCounter = _redeemableCounter.add(1);
}
// Emit token addition event.
emit AddedToken(msg.sender, _tokens[i], symbol, _magnitude[i], _loadable[i], _redeemable[i]);
}
}
/// @notice Remove ERC20 tokens from the whitelist of tokens.
/// @param _tokens ERC20 token contract addresses.
function removeTokens(address[] calldata _tokens) external onlyAdmin {
// Delete each token object from the list of supported tokens based on the addresses provided.
for (uint256 i = 0; i < _tokens.length; i++) {
// Store the token address.
address token = _tokens[i];
//token must be available, reverts on duplicates as well
require(_tokenInfoMap[token].available, "token is not available");
//if the token is redeemable decrease the redeemableCounter
if (_tokenInfoMap[token].redeemable) {
_redeemableCounter = _redeemableCounter.sub(1);
}
// Delete the token object.
delete _tokenInfoMap[token];
// Remove the token address from the address list.
for (uint256 j = 0; j < _tokenAddressArray.length.sub(1); j++) {
if (_tokenAddressArray[j] == token) {
_tokenAddressArray[j] = _tokenAddressArray[_tokenAddressArray.length.sub(1)];
break;
}
}
_tokenAddressArray.length--;
// Emit token removal event.
emit RemovedToken(msg.sender, token);
}
}
/// @notice based on the method it returns the recipient address and amount/value, ERC20 specific.
/// @param _data is the transaction payload.
function getERC20RecipientAndAmount(address _token, bytes calldata _data) external view returns (address, uint256) {
// Require that there exist enough bytes for encoding at least a method signature + data in the transaction payload:
// 4 (signature) + 32(address or uint256)
require(_data.length >= 4 + 32, "not enough method-encoding bytes");
// Get the method signature
bytes4 signature = _data._bytesToBytes4(0);
// Check if method Id is supported
require(isERC20MethodSupported(_token, signature), "unsupported method");
// returns the recipient's address and amount is the value to be transferred
if (signature == _BURN) {
// 4 (signature) + 32(uint256)
return (_token, _data._bytesToUint256(4));
} else if (signature == _TRANSFER_FROM) {
// 4 (signature) + 32(address) + 32(address) + 32(uint256)
require(_data.length >= 4 + 32 + 32 + 32, "not enough data for transferFrom");
return (_data._bytesToAddress(4 + 32 + 12), _data._bytesToUint256(4 + 32 + 32));
} else {
//transfer or approve
// 4 (signature) + 32(address) + 32(uint)
require(_data.length >= 4 + 32 + 32, "not enough data for transfer/appprove");
return (_data._bytesToAddress(4 + 12), _data._bytesToUint256(4 + 32));
}
}
/// @notice Toggles whether or not a token is loadable or not.
function setTokenLoadable(address _token, bool _loadable) external onlyAdmin {
// Require that the token exists.
require(_tokenInfoMap[_token].available, "token is not available");
// this sets the loadable flag to the value passed in
_tokenInfoMap[_token].loadable = _loadable;
emit UpdatedTokenLoadable(msg.sender, _token, _loadable);
}
/// @notice Toggles whether or not a token is redeemable or not.
function setTokenRedeemable(address _token, bool _redeemable) external onlyAdmin {
// Require that the token exists.
require(_tokenInfoMap[_token].available, "token is not available");
// this sets the redeemable flag to the value passed in
_tokenInfoMap[_token].redeemable = _redeemable;
emit UpdatedTokenRedeemable(msg.sender, _token, _redeemable);
}
/// @notice Update ERC20 token exchange rate.
/// @param _token ERC20 token contract address.
/// @param _rate ERC20 token exchange rate in wei.
/// @param _updateDate date for the token updates. This will be compared to when oracle updates are received.
function updateTokenRate(address _token, uint256 _rate, uint256 _updateDate) external onlyAdminOrOracle {
// Require that the token exists.
require(_tokenInfoMap[_token].available, "token is not available");
// Update the token's rate.
_tokenInfoMap[_token].rate = _rate;
// Update the token's last update timestamp.
_tokenInfoMap[_token].lastUpdate = _updateDate;
// Emit the rate update event.
emit UpdatedTokenRate(msg.sender, _token, _rate);
}
//// @notice Withdraw tokens from the smart contract to the specified account.
function claim(address payable _to, address _asset, uint256 _amount) external onlyAdmin {
_safeTransfer(_to, _asset, _amount);
emit Claimed(_to, _asset, _amount);
}
/// @notice This returns all of the fields for a given token.
/// @param _a is the address of a given token.
/// @return string of the token's symbol.
/// @return uint of the token's magnitude.
/// @return uint of the token's exchange rate to ETH.
/// @return bool whether the token is available.
/// @return bool whether the token is loadable to the TokenCard.
/// @return bool whether the token is redeemable to the TKN Holder Contract.
/// @return uint of the lastUpdated time of the token's exchange rate.
function getTokenInfo(address _a) external view returns (string memory, uint256, uint256, bool, bool, bool, uint256) {
Token storage tokenInfo = _tokenInfoMap[_a];
return (tokenInfo.symbol, tokenInfo.magnitude, tokenInfo.rate, tokenInfo.available, tokenInfo.loadable, tokenInfo.redeemable, tokenInfo.lastUpdate);
}
/// @notice This returns all of the fields for our StableCoin.
/// @return string of the token's symbol.
/// @return uint of the token's magnitude.
/// @return uint of the token's exchange rate to ETH.
/// @return bool whether the token is available.
/// @return bool whether the token is loadable to the TokenCard.
/// @return bool whether the token is redeemable to the TKN Holder Contract.
/// @return uint of the lastUpdated time of the token's exchange rate.
function getStablecoinInfo() external view returns (string memory, uint256, uint256, bool, bool, bool, uint256) {
Token storage stablecoinInfo = _tokenInfoMap[_stablecoin];
return (
stablecoinInfo.symbol,
stablecoinInfo.magnitude,
stablecoinInfo.rate,
stablecoinInfo.available,
stablecoinInfo.loadable,
stablecoinInfo.redeemable,
stablecoinInfo.lastUpdate
);
}
/// @notice This returns an array of all whitelisted token addresses.
/// @return address[] of whitelisted tokens.
function tokenAddressArray() external view returns (address[] memory) {
return _tokenAddressArray;
}
/// @notice This returns an array of all redeemable token addresses.
/// @return address[] of redeemable tokens.
function redeemableTokens() external view returns (address[] memory) {
address[] memory redeemableAddresses = new address[](_redeemableCounter);
uint256 redeemableIndex = 0;
for (uint256 i = 0; i < _tokenAddressArray.length; i++) {
address token = _tokenAddressArray[i];
if (_tokenInfoMap[token].redeemable) {
redeemableAddresses[redeemableIndex] = token;
redeemableIndex += 1;
}
}
return redeemableAddresses;
}
/// @notice This returns true if a method Id is supported for the specific token.
/// @return true if _methodId is supported in general or just for the specific token.
function isERC20MethodSupported(address _token, bytes4 _methodId) public view returns (bool) {
require(_tokenInfoMap[_token].available, "non-existing token");
return (_methodIdWhitelist[_methodId]);
}
/// @notice This returns true if the method is supported for all protected tokens.
/// @return true if _methodId is in the method whitelist.
function isERC20MethodWhitelisted(bytes4 _methodId) external view returns (bool) {
return (_methodIdWhitelist[_methodId]);
}
/// @notice This returns the number of redeemable tokens.
/// @return current # of redeemables.
function redeemableCounter() external view returns (uint256) {
return _redeemableCounter;
}
/// @notice This returns the address of our stablecoin of choice.
/// @return the address of the stablecoin contract.
function stablecoin() external view returns (address) {
return _stablecoin;
}
/// @notice this returns the node hash of our Oracle.
/// @return the oracle node registered in ENS.
function oracleNode() external view returns (bytes32) {
return _oracleNode;
}
}
/**
* The Consumer Contract Wallet
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./licence.sol";
import "./internals/ownable.sol";
import "./internals/controllable.sol";
import "./internals/balanceable.sol";
import "./internals/transferrable.sol";
import "./internals/ensResolvable.sol";
import "./internals/tokenWhitelistable.sol";
import "./externals/SafeMath.sol";
import "./externals/Address.sol";
import "./externals/ERC20.sol";
import "./externals/SafeERC20.sol";
import "./externals/ERC165.sol";
import "./externals/ECDSA.sol";
/// @title ControllableOwnable combines Controllable and Ownable
/// @dev providing an additional modifier to check if Owner or Controller
contract ControllableOwnable is Controllable, Ownable {
/// @dev Check if the sender is the Owner or one of the Controllers
modifier onlyOwnerOrController() {
require(_isOwner(msg.sender) || _isController(msg.sender), "only owner||controller");
_;
}
}
/// @title SelfCallableOwnable allows either owner or the contract itself to call its functions
/// @dev providing an additional modifier to check if Owner or self is calling
/// @dev the "self" here is used for the meta transactions
contract SelfCallableOwnable is Ownable {
/// @dev Check if the sender is the Owner or self
modifier onlyOwnerOrSelf() {
require(_isOwner(msg.sender) || msg.sender == address(this), "only owner||self");
_;
}
}
/// @title AddressWhitelist provides payee-whitelist functionality.
/// @dev This contract will allow the user to maintain a whitelist of addresses
/// @dev These addresses will live outside of the various spend limits
contract AddressWhitelist is ControllableOwnable, SelfCallableOwnable {
using SafeMath for uint256;
event AddedToWhitelist(address _sender, address[] _addresses);
event CancelledWhitelistAddition(address _sender, bytes32 _hash);
event SubmittedWhitelistAddition(address[] _addresses, bytes32 _hash);
event CancelledWhitelistRemoval(address _sender, bytes32 _hash);
event RemovedFromWhitelist(address _sender, address[] _addresses);
event SubmittedWhitelistRemoval(address[] _addresses, bytes32 _hash);
mapping(address => bool) public whitelistMap;
address[] public whitelistArray;
address[] private _pendingWhitelistAddition;
address[] private _pendingWhitelistRemoval;
bool public submittedWhitelistAddition;
bool public submittedWhitelistRemoval;
bool public isSetWhitelist;
/// @dev Check if the provided addresses contain the owner or the zero-address address.
modifier hasNoOwnerOrZeroAddress(address[] memory _addresses) {
for (uint256 i = 0; i < _addresses.length; i++) {
require(!_isOwner(_addresses[i]), "contains owner address");
require(_addresses[i] != address(0), "contains 0 address");
}
_;
}
/// @dev Check that neither addition nor removal operations have already been submitted.
modifier noActiveSubmission() {
require(!submittedWhitelistAddition && !submittedWhitelistRemoval, "whitelist sumbission pending");
_;
}
/// @dev Cancel pending whitelist addition.
function cancelWhitelistAddition(bytes32 _hash) external onlyOwnerOrController {
// Check if operation has been submitted.
require(submittedWhitelistAddition, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist addition match
require(_hash == calculateHash(_pendingWhitelistAddition), "non-matching pending whitelist hash");
// Reset pending addresses.
delete _pendingWhitelistAddition;
// Reset the submitted operation flag.
submittedWhitelistAddition = false;
// Emit the cancellation event.
emit CancelledWhitelistAddition(msg.sender, _hash);
}
/// @dev Cancel pending removal of whitelisted addresses.
function cancelWhitelistRemoval(bytes32 _hash) external onlyOwnerOrController {
// Check if operation has been submitted.
require(submittedWhitelistRemoval, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist removal match
require(_hash == calculateHash(_pendingWhitelistRemoval), "non-matching pending whitelist hash");
// Reset pending addresses.
delete _pendingWhitelistRemoval;
// Reset pending addresses.
submittedWhitelistRemoval = false;
// Emit the cancellation event.
emit CancelledWhitelistRemoval(msg.sender, _hash);
}
/// @dev Confirm pending whitelist addition.
/// @dev This will only ever be applied post 2FA, by one of the Controllers
/// @param _hash is the hash of the pending whitelist array, a form of lamport lock
function confirmWhitelistAddition(bytes32 _hash) external onlyController {
// Require that the whitelist addition has been submitted.
require(submittedWhitelistAddition, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist addition match
require(_hash == calculateHash(_pendingWhitelistAddition), "non-matching pending whitelist hash");
// Whitelist pending addresses.
for (uint256 i = 0; i < _pendingWhitelistAddition.length; i++) {
// check if it doesn't exist already.
if (!whitelistMap[_pendingWhitelistAddition[i]]) {
// add to the Map and the Array
whitelistMap[_pendingWhitelistAddition[i]] = true;
whitelistArray.push(_pendingWhitelistAddition[i]);
}
}
// Emit the addition event.
emit AddedToWhitelist(msg.sender, _pendingWhitelistAddition);
// Reset pending addresses.
delete _pendingWhitelistAddition;
// Reset the submission flag.
submittedWhitelistAddition = false;
}
/// @dev Confirm pending removal of whitelisted addresses.
function confirmWhitelistRemoval(bytes32 _hash) external onlyController {
// Require that the pending whitelist is not empty and the operation has been submitted.
require(submittedWhitelistRemoval, "no pending submission");
// Require that confirmation hash and the hash of the pending whitelist removal match
require(_hash == calculateHash(_pendingWhitelistRemoval), "non-matching pending whitelist hash");
// Remove pending addresses.
for (uint256 i = 0; i < _pendingWhitelistRemoval.length; i++) {
// check if it exists
if (whitelistMap[_pendingWhitelistRemoval[i]]) {
whitelistMap[_pendingWhitelistRemoval[i]] = false;
for (uint256 j = 0; j < whitelistArray.length.sub(1); j++) {
if (whitelistArray[j] == _pendingWhitelistRemoval[i]) {
whitelistArray[j] = whitelistArray[whitelistArray.length - 1];
break;
}
}
whitelistArray.length--;
}
}
// Emit the removal event.
emit RemovedFromWhitelist(msg.sender, _pendingWhitelistRemoval);
// Reset pending addresses.
delete _pendingWhitelistRemoval;
// Reset the submission flag.
submittedWhitelistRemoval = false;
}
/// @dev Getter for pending addition array.
function pendingWhitelistAddition() external view returns (address[] memory) {
return _pendingWhitelistAddition;
}
/// @dev Getter for pending removal array.
function pendingWhitelistRemoval() external view returns (address[] memory) {
return _pendingWhitelistRemoval;
}
/// @dev Add initial addresses to the whitelist.
/// @param _addresses are the Ethereum addresses to be whitelisted.
function setWhitelist(address[] calldata _addresses) external onlyOwnerOrSelf hasNoOwnerOrZeroAddress(_addresses) {
// Require that the whitelist has not been initialized.
require(!isSetWhitelist, "whitelist initialized");
// Add each of the provided addresses to the whitelist.
for (uint256 i = 0; i < _addresses.length; i++) {
// Dedup addresses before writing to the whitelist
if (!whitelistMap[_addresses[i]]) {
// adds to the whitelist mapping
whitelistMap[_addresses[i]] = true;
// adds to the whitelist array
whitelistArray.push(_addresses[i]);
}
}
isSetWhitelist = true;
// Emit the addition event.
emit AddedToWhitelist(msg.sender, whitelistArray);
}
/// @dev Add addresses to the whitelist.
/// @param _addresses are the Ethereum addresses to be whitelisted.
function submitWhitelistAddition(address[] calldata _addresses) external onlyOwnerOrSelf noActiveSubmission hasNoOwnerOrZeroAddress(_addresses) {
// Require that the whitelist has been initialized.
require(isSetWhitelist, "whitelist not initialized");
// Require this array of addresses not empty
require(_addresses.length > 0, "empty whitelist");
// Set the provided addresses to the pending addition addresses.
_pendingWhitelistAddition = _addresses;
// Flag the operation as submitted.
submittedWhitelistAddition = true;
// Emit the submission event.
emit SubmittedWhitelistAddition(_addresses, calculateHash(_addresses));
}
/// @dev Remove addresses from the whitelist.
/// @param _addresses are the Ethereum addresses to be removed.
function submitWhitelistRemoval(address[] calldata _addresses) external onlyOwnerOrSelf noActiveSubmission {
// Require that the whitelist has been initialized.
require(isSetWhitelist, "whitelist not initialized");
// Require that the array of addresses is not empty
require(_addresses.length > 0, "empty whitelist");
// Add the provided addresses to the pending addition list.
_pendingWhitelistRemoval = _addresses;
// Flag the operation as submitted.
submittedWhitelistRemoval = true;
// Emit the submission event.
emit SubmittedWhitelistRemoval(_addresses, calculateHash(_addresses));
}
/// @dev Method used to hash our whitelist address arrays.
function calculateHash(address[] memory _addresses) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_addresses));
}
}
/// @title DailyLimitTrait This trait allows for daily limits to be included in other contracts.
/// This contract will allow for a DailyLimit object to be instantiated and used.
library DailyLimitTrait {
using SafeMath for uint256;
event UpdatedAvailableLimit();
struct DailyLimit {
uint256 value;
uint256 available;
uint256 limitTimestamp;
uint256 pending;
bool controllerConfirmationRequired;
}
/// @dev Confirm pending set daily limit operation.
function _confirmLimitUpdate(DailyLimit storage self, uint256 _amount) internal {
// Require that pending and confirmed spend limit are the same
require(self.pending == _amount, "confirmed/submitted limit mismatch");
// Modify spend limit based on the pending value.
_modifyLimit(self, self.pending);
}
/// @dev Use up amount within the daily limit. Will fail if amount is larger than daily limit.
function _enforceLimit(DailyLimit storage self, uint256 _amount) internal {
// Account for the spend limit daily reset.
_updateAvailableLimit(self);
require(self.available >= _amount, "available<amount");
self.available = self.available.sub(_amount);
}
/// @dev Returns the available daily balance - accounts for daily limit reset.
/// @return amount of available to spend within the current day in base units.
function _getAvailableLimit(DailyLimit storage self) internal view returns (uint256) {
if (now > self.limitTimestamp.add(24 hours)) {
return self.value;
} else {
return self.available;
}
}
/// @dev Modify the spend limit and spend available based on the provided value.
/// @dev _amount is the daily limit amount in wei.
function _modifyLimit(DailyLimit storage self, uint256 _amount) private {
// Account for the spend limit daily reset.
_updateAvailableLimit(self);
// Set the daily limit to the provided amount.
self.value = _amount;
// Lower the available limit if it's higher than the new daily limit.
if (self.available > self.value) {
self.available = self.value;
}
}
/// @dev Set the daily limit.
/// @param _amount is the daily limit amount in base units.
function _setLimit(DailyLimit storage self, uint256 _amount) internal {
// Require that the spend limit has not been set yet.
require(!self.controllerConfirmationRequired, "limit already set");
// Modify spend limit based on the provided value.
_modifyLimit(self, _amount);
// Flag the operation as set.
self.controllerConfirmationRequired = true;
}
/// @dev Submit a daily limit update, needs to be confirmed.
/// @param _amount is the daily limit amount in base units.
function _submitLimitUpdate(DailyLimit storage self, uint256 _amount) internal {
// Require that the spend limit has been set.
require(self.controllerConfirmationRequired, "limit hasn't been set yet");
// Assign the provided amount to pending daily limit.
self.pending = _amount;
}
/// @dev Update available spend limit based on the daily reset.
function _updateAvailableLimit(DailyLimit storage self) private {
if (now > self.limitTimestamp.add(24 hours)) {
// Update the current timestamp.
self.limitTimestamp = now;
// Set the available limit to the current spend limit.
self.available = self.value;
emit UpdatedAvailableLimit();
}
}
}
/// @title it provides daily spend limit functionality.
contract SpendLimit is ControllableOwnable, SelfCallableOwnable {
event SetSpendLimit(address _sender, uint256 _amount);
event SubmittedSpendLimitUpdate(uint256 _amount);
using DailyLimitTrait for DailyLimitTrait.DailyLimit;
DailyLimitTrait.DailyLimit internal _spendLimit;
/// @dev Constructor initializes the daily spend limit in wei.
constructor(uint256 _limit_) internal {
_spendLimit = DailyLimitTrait.DailyLimit(_limit_, _limit_, now, 0, false);
}
/// @dev Confirm pending set daily limit operation.
function confirmSpendLimitUpdate(uint256 _amount) external onlyController {
_spendLimit._confirmLimitUpdate(_amount);
emit SetSpendLimit(msg.sender, _amount);
}
/// @dev Sets the initial daily spend (aka transfer) limit for non-whitelisted addresses.
/// @param _amount is the daily limit amount in wei.
function setSpendLimit(uint256 _amount) external onlyOwnerOrSelf {
_spendLimit._setLimit(_amount);
emit SetSpendLimit(msg.sender, _amount);
}
/// @dev View your available limit
function spendLimitAvailable() external view returns (uint256) {
return _spendLimit._getAvailableLimit();
}
/// @dev Is there an active spend limit change
function spendLimitPending() external view returns (uint256) {
return _spendLimit.pending;
}
/// @dev Has the spend limit been initialised
function spendLimitControllerConfirmationRequired() external view returns (bool) {
return _spendLimit.controllerConfirmationRequired;
}
/// @dev View how much has been spent already
function spendLimitValue() external view returns (uint256) {
return _spendLimit.value;
}
/// @dev Submit a daily transfer limit update for non-whitelisted addresses.
/// @param _amount is the daily limit amount in wei.
function submitSpendLimitUpdate(uint256 _amount) external onlyOwnerOrSelf {
_spendLimit._submitLimitUpdate(_amount);
emit SubmittedSpendLimitUpdate(_amount);
}
}
/// @title GasTopUpLimit provides daily limit functionality.
contract GasTopUpLimit is ControllableOwnable, SelfCallableOwnable {
event SetGasTopUpLimit(address _sender, uint256 _amount);
event SubmittedGasTopUpLimitUpdate(uint256 _amount);
uint256 private constant _MAXIMUM_GAS_TOPUP_LIMIT = 500 finney;
uint256 private constant _MINIMUM_GAS_TOPUP_LIMIT = 1 finney;
using DailyLimitTrait for DailyLimitTrait.DailyLimit;
DailyLimitTrait.DailyLimit internal _gasTopUpLimit;
/// @dev Constructor initializes the daily gas topup limit in wei.
constructor() internal {
_gasTopUpLimit = DailyLimitTrait.DailyLimit(_MAXIMUM_GAS_TOPUP_LIMIT, _MAXIMUM_GAS_TOPUP_LIMIT, now, 0, false);
}
/// @dev Confirm pending set top up gas limit operation.
function confirmGasTopUpLimitUpdate(uint256 _amount) external onlyController {
_gasTopUpLimit._confirmLimitUpdate(_amount);
emit SetGasTopUpLimit(msg.sender, _amount);
}
/// @dev View your available gas top-up limit
function gasTopUpLimitAvailable() external view returns (uint256) {
return _gasTopUpLimit._getAvailableLimit();
}
/// @dev Is there an active gas top-up limit change
function gasTopUpLimitPending() external view returns (uint256) {
return _gasTopUpLimit.pending;
}
/// @dev Has the gas top-up limit been initialised
function gasTopUpLimitControllerConfirmationRequired() external view returns (bool) {
return _gasTopUpLimit.controllerConfirmationRequired;
}
/// @dev View how much gas top-up has been spent already
function gasTopUpLimitValue() external view returns (uint256) {
return _gasTopUpLimit.value;
}
/// @dev Sets the daily gas top up limit.
/// @param _amount is the gas top up amount in wei.
function setGasTopUpLimit(uint256 _amount) external onlyOwnerOrSelf {
require(_MINIMUM_GAS_TOPUP_LIMIT <= _amount && _amount <= _MAXIMUM_GAS_TOPUP_LIMIT, "out of range top-up");
_gasTopUpLimit._setLimit(_amount);
emit SetGasTopUpLimit(msg.sender, _amount);
}
/// @dev Submit a daily gas top up limit update.
/// @param _amount is the daily top up gas limit amount in wei.
function submitGasTopUpLimitUpdate(uint256 _amount) external onlyOwnerOrSelf {
require(_MINIMUM_GAS_TOPUP_LIMIT <= _amount && _amount <= _MAXIMUM_GAS_TOPUP_LIMIT, "out of range top-up");
_gasTopUpLimit._submitLimitUpdate(_amount);
emit SubmittedGasTopUpLimitUpdate(_amount);
}
}
/// @title LoadLimit provides daily load limit functionality.
contract LoadLimit is ControllableOwnable, SelfCallableOwnable, TokenWhitelistable {
event SetLoadLimit(address _sender, uint256 _amount);
event SubmittedLoadLimitUpdate(uint256 _amount);
uint256 private constant _MAXIMUM_STABLECOIN_LOAD_LIMIT = 10000; // USD
uint256 private _maximumLoadLimit;
using DailyLimitTrait for DailyLimitTrait.DailyLimit;
DailyLimitTrait.DailyLimit internal _loadLimit;
constructor(bytes32 _tokenWhitelistNode_) internal TokenWhitelistable(_tokenWhitelistNode_) {
(, uint256 stablecoinMagnitude, , , , , ) = _getStablecoinInfo();
require(stablecoinMagnitude > 0, "no stablecoin");
_maximumLoadLimit = _MAXIMUM_STABLECOIN_LOAD_LIMIT * stablecoinMagnitude;
_loadLimit = DailyLimitTrait.DailyLimit(_maximumLoadLimit, _maximumLoadLimit, now, 0, false);
}
/// @dev Sets a daily card load limit.
/// @param _amount is the card load amount in current stablecoin base units.
function setLoadLimit(uint256 _amount) external onlyOwnerOrSelf {
require(_amount <= _maximumLoadLimit, "out of range load amount");
_loadLimit._setLimit(_amount);
emit SetLoadLimit(msg.sender, _amount);
}
/// @dev Submit a daily load limit update.
/// @param _amount is the daily load limit amount in wei.
function submitLoadLimitUpdate(uint256 _amount) external onlyOwnerOrSelf {
require(_amount <= _maximumLoadLimit, "out of range load amount");
_loadLimit._submitLimitUpdate(_amount);
emit SubmittedLoadLimitUpdate(_amount);
}
/// @dev Confirm pending set load limit operation.
function confirmLoadLimitUpdate(uint256 _amount) external onlyController {
_loadLimit._confirmLimitUpdate(_amount);
emit SetLoadLimit(msg.sender, _amount);
}
/// @dev View your available load limit
function loadLimitAvailable() external view returns (uint256) {
return _loadLimit._getAvailableLimit();
}
/// @dev Is there an active load limit change
function loadLimitPending() external view returns (uint256) {
return _loadLimit.pending;
}
/// @dev Has the load limit been initialised
function loadLimitControllerConfirmationRequired() external view returns (bool) {
return _loadLimit.controllerConfirmationRequired;
}
/// @dev View how much laod limit has been spent already
function loadLimitValue() external view returns (uint256) {
return _loadLimit.value;
}
}
/// @title Asset wallet with extra security features, gas top up management and card integration.
contract Wallet is ENSResolvable, GasTopUpLimit, LoadLimit, AddressWhitelist, SpendLimit, ERC165, Transferrable, Balanceable {
using Address for address;
using ECDSA for bytes32;
using SafeERC20 for ERC20;
using SafeMath for uint256;
event BulkTransferred(address _to, address[] _assets);
event ExecutedRelayedTransaction(bytes _data, bytes _returndata);
event ExecutedTransaction(address _destination, uint256 _value, bytes _data, bytes _returndata);
event IncreasedRelayNonce(address _sender, uint256 _currentNonce);
event LoadedTokenCard(address _asset, uint256 _amount);
event Received(address _from, uint256 _amount);
event ToppedUpGas(address _sender, address _owner, uint256 _amount);
event Transferred(address _to, address _asset, uint256 _amount);
event UpdatedAvailableLimit(); // This is here because our tests don't inherit events from a library
string public constant WALLET_VERSION = "3.2.0";
// keccak256("isValidSignature(bytes,bytes)") = 20c13b0bc670c284a9f19cdf7a533ca249404190f8dc132aac33e733b965269e
bytes4 private constant _EIP_1271 = 0x20c13b0b;
// keccak256("isValidSignature(bytes32,bytes)") = 1626ba7e356f5979dd355a3d2bfb43e80420a480c3b854edce286a82d7496869
bytes4 private constant _EIP_1654 = 0x1626ba7e;
/// @dev Supported ERC165 interface ID.
bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; // solium-disable-line uppercase
/// @dev this is an internal nonce to prevent replay attacks from relayer
uint256 public relayNonce;
/// @dev Is the registered ENS node identifying the licence contract.
bytes32 private _licenceNode;
/// @dev Constructor initializes the wallet top up limit and the vault contract.
/// @param _owner_ is the owner account of the wallet contract.
/// @param _transferable_ indicates whether the contract ownership can be transferred.
/// @param _ens_ is the address of the ENS registry.
/// @param _tokenWhitelistNode_ is the ENS name node of the Token whitelist.
/// @param _controllerNode_ is the ENS name node of the Controller contract.
/// @param _licenceNode_ is the ENS name node of the Licence contract.
/// @param _spendLimit_ is the initial spend limit.
constructor(
address payable _owner_,
bool _transferable_,
address _ens_,
bytes32 _tokenWhitelistNode_,
bytes32 _controllerNode_,
bytes32 _licenceNode_,
uint256 _spendLimit_
) public ENSResolvable(_ens_) SpendLimit(_spendLimit_) Ownable(_owner_, _transferable_) Controllable(_controllerNode_) LoadLimit(_tokenWhitelistNode_) {
_licenceNode = _licenceNode_;
}
/// @dev Checks if the value is not zero.
modifier isNotZero(uint256 _value) {
require(_value != 0, "value=0");
_;
}
/// @dev Ether can be deposited from any source, so this contract must be payable by anyone.
function() external payable {
emit Received(msg.sender, msg.value);
}
/// @dev This is a bulk transfer convenience function, used to migrate contracts.
/// @notice If any of the transfers fail, this will revert.
/// @param _to is the recipient's address, can't be the zero (0x0) address: transfer() will revert.
/// @param _assets is an array of addresses of ERC20 tokens or 0x0 for ether.
function bulkTransfer(address payable _to, address[] calldata _assets) external onlyOwnerOrSelf {
// check to make sure that _assets isn't empty
require(_assets.length != 0, "asset array is empty");
// This loops through all of the transfers to be made
for (uint256 i = 0; i < _assets.length; i++) {
uint256 amount = _balance(address(this), _assets[i]);
// use our safe, daily limit protected transfer
transfer(_to, _assets[i], amount);
}
emit BulkTransferred(_to, _assets);
}
/// @dev This function allows for the controller to relay transactions on the owner's behalf,
/// the relayed message has to be signed by the owner.
/// @param _nonce only used for relayed transactions, must match the wallet's relayNonce.
/// @param _data abi encoded data payload.
/// @param _signature signed prefix + data.
function executeRelayedTransaction(uint256 _nonce, bytes calldata _data, bytes calldata _signature) external onlyController {
// Expecting prefixed data ("monolith:") indicating relayed transaction...
// ...and an Ethereum Signed Message to protect user from signing an actual Tx
uint256 id;
assembly {
id := chainid() //1 for Ethereum mainnet, > 1 for public testnets.
}
bytes32 dataHash = keccak256(abi.encodePacked("monolith:", id, address(this), _nonce, _data)).toEthSignedMessageHash();
// Verify signature validity i.e. signer == owner
require(isValidSignature(dataHash, _signature) == _EIP_1654, "sig not valid");
// Verify and increase relayNonce to prevent replay attacks from the relayer
require(_nonce == relayNonce, "tx replay");
_increaseRelayNonce();
// invoke wallet function with an external call
(bool success, bytes memory returndata) = address(this).call(_data);
require(success, string(returndata));
emit ExecutedRelayedTransaction(_data, returndata);
}
/// @dev This allows the user to cancel a transaction that was unexpectedly delayed by the relayer
function increaseRelayNonce() external onlyOwner {
_increaseRelayNonce();
}
/// @dev This bumps the relayNonce and emits an event accordingly
function _increaseRelayNonce() internal {
relayNonce++;
emit IncreasedRelayNonce(msg.sender, relayNonce);
}
/// @dev Implements EIP-1271: receives the raw data (bytes)
/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1271.md
/// @param _data Arbitrary length data signed on the behalf of address(this)
/// @param _signature Signature byte array associated with _data
function isValidSignature(bytes calldata _data, bytes calldata _signature) external view returns (bytes4) {
bytes32 dataHash = keccak256(abi.encodePacked(_data));
// isValidSignature call reverts if not valid.
require(isValidSignature(dataHash, _signature) == _EIP_1654, "sig not valid");
return _EIP_1271;
}
/// @return licence contract node registered in ENS.
function licenceNode() external view returns (bytes32) {
return _licenceNode;
}
/// @dev Load a token card with the specified asset amount.
/// @dev the amount send should be inclusive of the percent licence.
/// @param _asset is the address of an ERC20 token or 0x0 for ether.
/// @param _amount is the amount of assets to be transferred in base units.
function loadTokenCard(address _asset, uint256 _amount) external payable onlyOwnerOrSelf {
// check if token is allowed to be used for loading the card
require(_isTokenLoadable(_asset), "token not loadable");
// Convert token amount to stablecoin value.
uint256 stablecoinValue = convertToStablecoin(_asset, _amount);
// Check against the daily spent limit and update accordingly, require that the value is under remaining limit.
_loadLimit._enforceLimit(stablecoinValue);
// Get the TKN licenceAddress from ENS
address licenceAddress = _ensResolve(_licenceNode);
if (_asset != address(0)) {
ERC20(_asset).safeApprove(licenceAddress, _amount);
ILicence(licenceAddress).load(_asset, _amount);
} else {
ILicence(licenceAddress).load.value(_amount)(_asset, _amount);
}
emit LoadedTokenCard(_asset, _amount);
}
/// @dev Checks for interface support based on ERC165.
function supportsInterface(bytes4 _interfaceID) external view returns (bool) {
return _interfaceID == _ERC165_INTERFACE_ID;
}
/// @dev Refill owner's gas balance, revert if the transaction amount is too large
/// @param _amount is the amount of ether to transfer to the owner account in wei.
function topUpGas(uint256 _amount) external isNotZero(_amount) onlyOwnerOrController {
// Check against the daily spent limit and update accordingly, require that the value is under remaining limit.
_gasTopUpLimit._enforceLimit(_amount);
// Then perform the transfer
owner().transfer(_amount);
// Emit the gas top up event.
emit ToppedUpGas(msg.sender, owner(), _amount);
}
/// @dev This function allows for the wallet to send a batch of transactions instead of one,
/// it calls executeTransaction() so that the daily limit is enforced.
/// @param _transactionBatch data encoding the transactions to be sent,
/// following executeTransaction's format i.e. (destination, value, data)
function batchExecuteTransaction(bytes memory _transactionBatch) public onlyOwnerOrSelf {
uint256 batchLength = _transactionBatch.length + 32; // because the pos starts from 32
uint256 remainingBytesLength = _transactionBatch.length; // remaining bytes to be processed
uint256 pos = 32; //the first 32 bytes denote the byte array length, start from actual data
address destination; // destination address
uint256 value; // trasanction value
uint256 dataLength; // externall call data length
bytes memory data; // call data
while (pos < batchLength) {
// there should always be at least 84 bytes remaining: the minimun #bytes required to encode a Tx
remainingBytesLength = remainingBytesLength.sub(84);
assembly {
// shift right by 96 bits (256 - 160) to get the destination address (and zero the excessive bytes)
destination := shr(96, mload(add(_transactionBatch, pos)))
// get value: pos + 20 bytes (destinnation address)
value := mload(add(_transactionBatch, add(pos, 20)))
// get data: pos + 20 (destination address) + 32 (value) bytes
// the first 32 bytes denote the byte array length
dataLength := mload(add(_transactionBatch, add(pos, 52)))
data := add(_transactionBatch, add(pos, 52))
}
// pos += 20 + 32 + 32 + dataLength, reverts in case of overflow
pos = pos.add(dataLength).add(84);
// revert in case the encoded dataLength is gonna cause an out of bound access
require(pos <= batchLength, "out of bounds");
// if length is 0 ignore the data field
if (dataLength == 0) {
data = bytes("");
}
// call executeTransaction(), if one of them reverts then the whole batch reverts.
executeTransaction(destination, value, data);
}
}
/// @dev Convert ERC20 token amount to the corresponding ether amount.
/// @param _token ERC20 token contract address.
/// @param _amount amount of token in base units.
function convertToEther(address _token, uint256 _amount) public view returns (uint256) {
// Store the token in memory to save map entry lookup gas.
(, uint256 magnitude, uint256 rate, bool available, , , ) = _getTokenInfo(_token);
// If the token exists require that its rate is not zero.
if (available) {
require(rate != 0, "rate=0");
// Safely convert the token amount to ether based on the exchange rate.
return _amount.mul(rate).div(magnitude);
}
return 0;
}
/// @dev Convert ether or ERC20 token amount to the corresponding stablecoin amount.
/// @param _token ERC20 token contract address.
/// @param _amount amount of token in base units.
function convertToStablecoin(address _token, uint256 _amount) public view returns (uint256) {
// avoid the unnecessary calculations if the token to be loaded is the stablecoin itself
if (_token == _stablecoin()) {
return _amount;
}
uint256 amountToSend = _amount;
// 0x0 represents ether
if (_token != address(0)) {
// convert to eth first, same as convertToEther()
// Store the token in memory to save map entry lookup gas.
(, uint256 magnitude, uint256 rate, bool available, , , ) = _getTokenInfo(_token);
// require that token both exists in the whitelist and its rate is not zero.
require(available, "token not available");
require(rate != 0, "rate=0");
// Safely convert the token amount to ether based on the exchangeonly rate.
amountToSend = _amount.mul(rate).div(magnitude);
}
// _amountToSend now is in ether
// Get the stablecoin's magnitude and its current rate.
(, uint256 stablecoinMagnitude, uint256 stablecoinRate, bool stablecoinAvailable, , , ) = _getStablecoinInfo();
// Check if the stablecoin rate is set.
require(stablecoinAvailable, "token not available");
require(stablecoinRate != 0, "stablecoin rate=0");
// Safely convert the token amount to stablecoin based on its exchange rate and the stablecoin exchange rate.
return amountToSend.mul(stablecoinMagnitude).div(stablecoinRate);
}
/// @dev This function allows for the owner to send any transaction from the Wallet to arbitrary addresses
/// @param _destination address of the transaction
/// @param _value ETH amount in wei
/// @param _data transaction payload binary
function executeTransaction(address _destination, uint256 _value, bytes memory _data) public onlyOwnerOrSelf returns (bytes memory) {
// If value is send across as a part of this executeTransaction, this will be sent to any payable
// destination. As a result enforceLimit if destination is not whitelisted.
if (!whitelistMap[_destination]) {
_spendLimit._enforceLimit(_value);
}
// Check if the destination is a Contract and it is one of our supported tokens
if (address(_destination).isContract() && _isTokenAvailable(_destination)) {
// to is the recipient's address and amount is the value to be transferred
address to;
uint256 amount;
(to, amount) = _getERC20RecipientAndAmount(_destination, _data);
if (!whitelistMap[to]) {
// If the address (of the token contract, e.g) is not in the TokenWhitelist used by the convert method
// then etherValue will be zero
uint256 etherValue = convertToEther(_destination, amount);
_spendLimit._enforceLimit(etherValue);
}
// use callOptionalReturn provided in SafeERC20 in case the ERC20 method
// returns false instead of reverting!
ERC20(_destination).callOptionalReturn(_data);
// if ERC20 call completes, return a boolean "true" as bytes emulating ERC20
bytes memory b = new bytes(32);
b[31] = 0x01;
emit ExecutedTransaction(_destination, _value, _data, b);
return b;
}
(bool success, bytes memory returndata) = _destination.call.value(_value)(_data);
require(success, string(returndata));
emit ExecutedTransaction(_destination, _value, _data, returndata);
// returns all of the bytes returned by _destination contract
return returndata;
}
/// @dev Implements EIP-1654: receives the hashed message(bytes32)
/// https://github.com/ethereum/EIPs/issues/1654.md
/// @param _hashedData Hashed data signed on the behalf of address(this)
/// @param _signature Signature byte array associated with _dataHash
function isValidSignature(bytes32 _hashedData, bytes memory _signature) public view returns (bytes4) {
address from = _hashedData.recover(_signature);
require(_isOwner(from), "invalid signature");
return _EIP_1654;
}
/// @dev Transfers the specified asset to the recipient's address.
/// @param _to is the recipient's address.
/// @param _asset is the address of an ERC20 token or 0x0 for ether.
/// @param _amount is the amount of assets to be transferred in base units.
function transfer(address payable _to, address _asset, uint256 _amount) public onlyOwnerOrSelf isNotZero(_amount) {
// Checks if the _to address is not the zero-address
require(_to != address(0), "destination=0");
// If address is not whitelisted, take daily limit into account.
if (!whitelistMap[_to]) {
// initialize ether value in case the asset is ETH
uint256 etherValue = _amount;
// Convert token amount to ether value if asset is an ERC20 token.
if (_asset != address(0)) {
etherValue = convertToEther(_asset, _amount);
}
// Check against the daily spent limit and update accordingly
// Check against the daily spent limit and update accordingly, require that the value is under remaining limit.
_spendLimit._enforceLimit(etherValue);
}
// Transfer token or ether based on the provided address.
_safeTransfer(_to, _asset, _amount);
// Emit the transfer event.
emit Transferred(_to, _asset, _amount);
}
}
/**
* The Consumer Contract Wallet - Wallet Deployer Cache
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./wallet.sol";
import "./internals/ensResolvable.sol";
import "./internals/controllable.sol";
/// @title IWalletCache interface describes a method for poping an already cached wallet
interface IWalletCache {
function walletCachePop() external returns (Wallet);
}
//// @title Wallet cache with wallet pre-caching functionality.
contract WalletCache is ENSResolvable, Controllable {
event CachedWallet(Wallet _wallet);
/***** Constants *****/
// Default values for mainnet ENS
// licence.tokencard.eth
bytes32 private constant _DEFAULT_LICENCE_NODE = 0xd0ff8bd67f6e25e4e4b010df582a36a0ee9b78e49afe6cc1cff5dd5a83040330;
// token-whitelist.tokencard.eth
bytes32 private constant _DEFAULT_TOKEN_WHITELIST_NODE = 0xe84f90570f13fe09f288f2411ff9cf50da611ed0c7db7f73d48053ffc974d396;
// wallet-deployer.v3.tokencard.eth
bytes32 private constant _DEFAULT_WALLET_DEPLOYER_NODE = 0x1d0c0adbe6addd93659446311e0767a56b67d41ef38f0cb66dcf7560d28a5a38;
bytes32 public licenceNode = _DEFAULT_LICENCE_NODE;
bytes32 public tokenWhitelistNode = _DEFAULT_TOKEN_WHITELIST_NODE;
bytes32 public walletDeployerNode = _DEFAULT_WALLET_DEPLOYER_NODE;
Wallet[] public cachedWallets;
address public ens;
uint256 public defaultSpendLimit;
/// @notice parameters are passed in so that they can be used to construct new instances of the wallet
/// @dev pass in bytes32 to use the default, production node labels for ENS
constructor(
address _ens_,
uint256 _defaultSpendLimit_,
bytes32 _controllerNode_,
bytes32 _licenceNode_,
bytes32 _tokenWhitelistNode_,
bytes32 _walletDeployerNode_
) public ENSResolvable(_ens_) Controllable(_controllerNode_) {
ens = _ens_;
defaultSpendLimit = _defaultSpendLimit_;
// Set licenceNode or use default
if (_licenceNode_ != bytes32(0)) {
licenceNode = _licenceNode_;
}
// Set tokenWhitelistNode or use default
if (_tokenWhitelistNode_ != bytes32(0)) {
tokenWhitelistNode = _tokenWhitelistNode_;
}
// Set walletDeployerNode or use default
if (_walletDeployerNode_ != bytes32(0)) {
walletDeployerNode = _walletDeployerNode_;
}
}
modifier onlyWalletDeployer() {
require(msg.sender == _ensResolve(walletDeployerNode), "not called by wallet-deployer");
_;
}
/// @notice This public method allows anyone to pre-cache wallets
function cacheWallet() public {
// the address(uint160()) cast is done as the Wallet owner (1st argument) needs to be payable
Wallet wallet = new Wallet(
address(uint160(_ensResolve(walletDeployerNode))),
true,
ens,
tokenWhitelistNode,
controllerNode(),
licenceNode,
defaultSpendLimit
);
cachedWallets.push(wallet);
emit CachedWallet(wallet);
}
/// @notice This public method allows only the wallet deployer to pop pre-cached wallets or create a new one in case there aren't any
function walletCachePop() external onlyWalletDeployer returns (Wallet) {
if (cachedWallets.length < 1) {
cacheWallet();
}
Wallet wallet = cachedWallets[cachedWallets.length - 1];
cachedWallets.pop();
return wallet;
}
/// @notice returns the number of pre-cached wallets
function cachedWalletsCount() external view returns (uint256) {
return cachedWallets.length;
}
}
/**
* The Consumer Contract Wallet - Wallet Deployer
* Copyright (C) 2019 The Contract Wallet Company Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity ^0.5.17;
import "./wallet.sol";
import "./walletCache.sol";
import "./internals/controllable.sol";
//// @title Wallet deployer with pre-caching if wallets functionality.
contract WalletDeployer is ENSResolvable, Controllable {
event DeployedWallet(Wallet _wallet, address _owner);
event MigratedWallet(Wallet _wallet, Wallet _oldWallet, address _owner, uint256 _paid);
/***** Constants *****/
// Default values for mainnet ENS
// wallet-cache.v3.tokencard.eth
bytes32 private constant _DEFAULT_WALLET_CACHE_NODE = 0xaf553cb0d77690819f9d6fbaa04416e1fdcfa01b2a9a833c7a11e6ae0bc1be88;
bytes32 public walletCacheNode = _DEFAULT_WALLET_CACHE_NODE;
mapping(address => address) public deployedWallets;
/// @notice it needs to know to address of the wallet cache
constructor(address _ens_, bytes32 _controllerNode_, bytes32 _walletCacheNode_) public ENSResolvable(_ens_) Controllable(_controllerNode_) {
// Set walletCacheNode or use default
if (_walletCacheNode_ != bytes32(0)) {
walletCacheNode = _walletCacheNode_;
}
}
/// @notice This function is used to deploy a Wallet for a given owner address
/// @param _owner is the owner address for the new Wallet to be
function deployWallet(address payable _owner) external onlyController {
Wallet wallet = IWalletCache(_ensResolve(walletCacheNode)).walletCachePop();
emit DeployedWallet(wallet, _owner);
deployedWallets[_owner] = address(wallet);
// This sets the changeableOwner boolean to false, i.e. no more change ownership
wallet.transferOwnership(_owner, false);
}
/// @notice This function is used to migrate an owner's security settings from a previous version of the wallet
/// @param _owner is the owner address for the new Wallet to be
/// @param _spendLimit is the user's set daily spend limit
/// @param _gasTopUpLimit is the user's set daily gas top-up limit
/// @param _whitelistedAddresses is the set of the user's whitelisted addresses
function migrateWallet(
address payable _owner,
Wallet _oldWallet,
bool _initializedSpendLimit,
bool _initializedGasTopUpLimit,
bool _initializedLoadLimit,
bool _initializedWhitelist,
uint256 _spendLimit,
uint256 _gasTopUpLimit,
uint256 _loadLimit,
address[] calldata _whitelistedAddresses
) external payable onlyController {
require(deployedWallets[_owner] == address(0x0), "wallet already deployed for owner");
require(_oldWallet.owner() == _owner, "owner mismatch");
Wallet wallet = IWalletCache(_ensResolve(walletCacheNode)).walletCachePop();
emit MigratedWallet(wallet, _oldWallet, _owner, msg.value);
deployedWallets[_owner] = address(wallet);
// Sets up the security settings as per the _oldWallet
if (_initializedSpendLimit) {
wallet.setSpendLimit(_spendLimit);
}
if (_initializedGasTopUpLimit) {
wallet.setGasTopUpLimit(_gasTopUpLimit);
}
if (_initializedLoadLimit) {
wallet.setLoadLimit(_loadLimit);
}
if (_initializedWhitelist) {
wallet.setWhitelist(_whitelistedAddresses);
}
wallet.transferOwnership(_owner, false);
if (msg.value > 0) {
_owner.transfer(msg.value);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment