Created
May 27, 2020 23:33
-
-
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=
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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