diff --git a/packages/contracts-watr/.env.deploy.example b/packages/contracts-watr/.env.deploy.example new file mode 100644 index 000000000..d7d9ab782 --- /dev/null +++ b/packages/contracts-watr/.env.deploy.example @@ -0,0 +1 @@ +TRUE_USD_ASSET_ID=1983 diff --git a/packages/contracts-watr/.env.test.example b/packages/contracts-watr/.env.test.example new file mode 100644 index 000000000..87367bb2f --- /dev/null +++ b/packages/contracts-watr/.env.test.example @@ -0,0 +1,2 @@ +# required to run `yarn verify:deployments` +PRIVATE_KEY_DEPLOYER=private_key diff --git a/packages/contracts-watr/.gitignore b/packages/contracts-watr/.gitignore index 89a888b43..acce6f886 100644 --- a/packages/contracts-watr/.gitignore +++ b/packages/contracts-watr/.gitignore @@ -2,3 +2,6 @@ /build /cache /flattened_contracts +deployments-watr_local.json +.env.test +.env.deploy diff --git a/packages/contracts-watr/contracts/TokenControllerV3.sol b/packages/contracts-watr/contracts/TokenControllerV3.sol index 067f5916b..9d3f0059e 100644 --- a/packages/contracts-watr/contracts/TokenControllerV3.sol +++ b/packages/contracts-watr/contracts/TokenControllerV3.sol @@ -85,7 +85,7 @@ contract TokenControllerV3 { // paused version of TrueCurrency in Production // pausing the contract upgrades the proxy to this implementation - address public constant PAUSED_IMPLEMENTATION = 0x3c8984DCE8f68FCDEEEafD9E0eca3598562eD291; + address public pausedImplementation; modifier onlyMintKeyOrOwner() { require(msg.sender == mintKey || msg.sender == owner, "must be mintKey or owner"); @@ -184,10 +184,11 @@ contract TokenControllerV3 { _; } - function initialize() external { + function initialize(address _pausedImplementation) external { require(!initialized, "already initialized"); - owner = msg.sender; initialized = true; + pausedImplementation = _pausedImplementation; + owner = msg.sender; } /** @@ -592,7 +593,7 @@ contract TokenControllerV3 { * @dev pause all pausable actions on TrueCurrency, mints/burn/transfer/approve */ function pauseToken() external virtual onlyOwner { - IOwnedUpgradeabilityProxy(address(token)).upgradeTo(PAUSED_IMPLEMENTATION); + IOwnedUpgradeabilityProxy(address(token)).upgradeTo(pausedImplementation); } /** diff --git a/packages/contracts-watr/contracts/TrueCurrency.sol b/packages/contracts-watr/contracts/TrueCurrency.sol index a8086c9fd..a55fc1209 100644 --- a/packages/contracts-watr/contracts/TrueCurrency.sol +++ b/packages/contracts-watr/contracts/TrueCurrency.sol @@ -2,6 +2,7 @@ pragma solidity 0.6.10; import {BurnableTokenWithBounds} from "./common/BurnableTokenWithBounds.sol"; +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; /** * @title TrueCurrency @@ -39,6 +40,8 @@ import {BurnableTokenWithBounds} from "./common/BurnableTokenWithBounds.sol"; * - ERC20 Tokens and Ether sent to this contract can be reclaimed by the owner */ abstract contract TrueCurrency is BurnableTokenWithBounds { + using SafeMath for uint256; + uint256 constant CENT = 10**16; uint256 constant REDEMPTION_ADDRESS_COUNT = 0x100000; @@ -54,10 +57,11 @@ abstract contract TrueCurrency is BurnableTokenWithBounds { */ event Mint(address indexed to, uint256 value); - function initialize() external { + function initialize(address _nativeToken) external { require(!initialized, "already initialized"); - owner = msg.sender; initialized = true; + owner = msg.sender; + nativeToken = _nativeToken; } /** @@ -126,8 +130,9 @@ abstract contract TrueCurrency is BurnableTokenWithBounds { require(!isBlacklisted[recipient], "TrueCurrency: recipient is blacklisted"); if (isRedemptionAddress(recipient)) { - super._transfer(sender, recipient, amount.sub(amount.mod(CENT))); - _burn(recipient, amount.sub(amount.mod(CENT))); + uint256 _amount = amount.sub(amount.mod(CENT)); + super._transfer(sender, recipient, _amount); + _burn(recipient, _amount); } else { super._transfer(sender, recipient, amount); } diff --git a/packages/contracts-watr/contracts/common/ERC20.sol b/packages/contracts-watr/contracts/common/ERC20.sol deleted file mode 100644 index 4b52bf193..000000000 --- a/packages/contracts-watr/contracts/common/ERC20.sol +++ /dev/null @@ -1,284 +0,0 @@ -/** - * @notice This is a copy of openzeppelin ERC20 contract with removed state variables. - * Removing state variables has been necessary due to proxy pattern usage. - * Changes to Openzeppelin ERC20 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/de99bccbfd4ecd19d7369d01b070aa72c64423c9/contracts/token/ERC20/ERC20.sol: - * - Remove state variables _name, _symbol, _decimals - * - Use state variables _balances, _allowances, _totalSupply from ProxyStorage - * - Remove constructor - * - Solidity version changed from ^0.6.0 to 0.6.10 - * - Contract made abstract - * - * See also: ClaimableOwnable.sol and ProxyStorage.sol - */ - -// SPDX-License-Identifier: MIT - -pragma solidity 0.6.10; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Context} from "@openzeppelin/contracts/GSN/Context.sol"; -import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -import {ClaimableOwnable} from "./ClaimableOwnable.sol"; - -// prettier-ignore -/** - * @dev Implementation of the {IERC20} interface. - * - * This implementation is agnostic to the way tokens are created. This means - * that a supply mechanism has to be added in a derived contract using {_mint}. - * For a generic mechanism see {ERC20PresetMinterPauser}. - * - * TIP: For a detailed writeup see our guide - * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How - * to implement supply mechanisms]. - * - * We have followed general OpenZeppelin guidelines: functions revert instead - * of returning `false` on failure. This behavior is nonetheless conventional - * and does not conflict with the expectations of ERC20 applications. - * - * Additionally, an {Approval} event is emitted on calls to {transferFrom}. - * This allows applications to reconstruct the allowance for all accounts just - * by listening to said events. Other implementations of the EIP may not emit - * these events, as it isn't required by the specification. - * - * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} - * functions have been added to mitigate the well-known issues around setting - * allowances. See {IERC20-approve}. - */ -abstract contract ERC20 is ClaimableOwnable, Context, IERC20 { - using SafeMath for uint256; - using Address for address; - - /** - * @dev Returns the name of the token. - */ - function name() public virtual pure returns (string memory); - - /** - * @dev Returns the symbol of the token, usually a shorter version of the - * name. - */ - function symbol() public virtual pure returns (string memory); - - /** - * @dev Returns the number of decimals used to get its user representation. - * For example, if `decimals` equals `2`, a balance of `505` tokens should - * be displayed to a user as `5,05` (`505 / 10 ** 2`). - * - * Tokens usually opt for a value of 18, imitating the relationship between - * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is - * called. - * - * NOTE: This information is only used for _display_ purposes: it in - * no way affects any of the arithmetic of the contract, including - * {IERC20-balanceOf} and {IERC20-transfer}. - */ - function decimals() public virtual pure returns (uint8) { - return 18; - } - - /** - * @dev See {IERC20-totalSupply}. - */ - function totalSupply() public view virtual override returns (uint256) { - return _totalSupply; - } - - /** - * @dev See {IERC20-balanceOf}. - */ - function balanceOf(address account) public view virtual override returns (uint256) { - return _balances[account]; - } - - /** - * @dev See {IERC20-transfer}. - * - * Requirements: - * - * - `recipient` cannot be the zero address. - * - the caller must have a balance of at least `amount`. - */ - function transfer(address recipient, uint256 amount) public virtual override returns (bool) { - _transfer(_msgSender(), recipient, amount); - return true; - } - - /** - * @dev See {IERC20-allowance}. - */ - function allowance(address owner, address spender) public view virtual override returns (uint256) { - return _allowances[owner][spender]; - } - - /** - * @dev See {IERC20-approve}. - * - * Requirements: - * - * - `spender` cannot be the zero address. - */ - function approve(address spender, uint256 amount) public virtual override returns (bool) { - _approve(_msgSender(), spender, amount); - return true; - } - - /** - * @dev See {IERC20-transferFrom}. - * - * Emits an {Approval} event indicating the updated allowance. This is not - * required by the EIP. See the note at the beginning of {ERC20}; - * - * Requirements: - * - `sender` and `recipient` cannot be the zero address. - * - `sender` must have a balance of at least `amount`. - * - the caller must have allowance for ``sender``'s tokens of at least - * `amount`. - */ - function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { - _transfer(sender, recipient, amount); - _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); - return true; - } - - /** - * @dev Atomically increases the allowance granted to `spender` by the caller. - * - * This is an alternative to {approve} that can be used as a mitigation for - * problems described in {IERC20-approve}. - * - * Emits an {Approval} event indicating the updated allowance. - * - * Requirements: - * - * - `spender` cannot be the zero address. - */ - function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { - _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); - return true; - } - - /** - * @dev Atomically decreases the allowance granted to `spender` by the caller. - * - * This is an alternative to {approve} that can be used as a mitigation for - * problems described in {IERC20-approve}. - * - * Emits an {Approval} event indicating the updated allowance. - * - * Requirements: - * - * - `spender` cannot be the zero address. - * - `spender` must have allowance for the caller of at least - * `subtractedValue`. - */ - function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { - _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); - return true; - } - - /** - * @dev Moves tokens `amount` from `sender` to `recipient`. - * - * This is internal function is equivalent to {transfer}, and can be used to - * e.g. implement automatic token fees, slashing mechanisms, etc. - * - * Emits a {Transfer} event. - * - * Requirements: - * - * - `sender` cannot be the zero address. - * - `recipient` cannot be the zero address. - * - `sender` must have a balance of at least `amount`. - */ - function _transfer(address sender, address recipient, uint256 amount) internal virtual { - require(sender != address(0), "ERC20: transfer from the zero address"); - require(recipient != address(0), "ERC20: transfer to the zero address"); - - _beforeTokenTransfer(sender, recipient, amount); - - _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); - _balances[recipient] = _balances[recipient].add(amount); - emit Transfer(sender, recipient, amount); - } - - /** @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * Emits a {Transfer} event with `from` set to the zero address. - * - * Requirements - * - * - `to` cannot be the zero address. - */ - function _mint(address account, uint256 amount) internal virtual { - require(account != address(0), "ERC20: mint to the zero address"); - - _beforeTokenTransfer(address(0), account, amount); - - _totalSupply = _totalSupply.add(amount); - _balances[account] = _balances[account].add(amount); - emit Transfer(address(0), account, amount); - } - - /** - * @dev Destroys `amount` tokens from `account`, reducing the - * total supply. - * - * Emits a {Transfer} event with `to` set to the zero address. - * - * Requirements - * - * - `account` cannot be the zero address. - * - `account` must have at least `amount` tokens. - */ - function _burn(address account, uint256 amount) internal virtual { - require(account != address(0), "ERC20: burn from the zero address"); - - _beforeTokenTransfer(account, address(0), amount); - - _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); - _totalSupply = _totalSupply.sub(amount); - emit Transfer(account, address(0), amount); - } - - /** - * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. - * - * This is internal function is equivalent to `approve`, and can be used to - * e.g. set automatic allowances for certain subsystems, etc. - * - * Emits an {Approval} event. - * - * Requirements: - * - * - `owner` cannot be the zero address. - * - `spender` cannot be the zero address. - */ - function _approve(address owner, address spender, uint256 amount) internal virtual { - require(owner != address(0), "ERC20: approve from the zero address"); - require(spender != address(0), "ERC20: approve to the zero address"); - - _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); - } - - /** - * @dev Hook that is called before any transfer of tokens. This includes - * minting and burning. - * - * Calling conditions: - * - * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens - * will be to transferred to `to`. - * - when `from` is zero, `amount` tokens will be minted for `to`. - * - when `to` is zero, `amount` of ``from``'s tokens will be burned. - * - `from` and `to` are never both zero. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. - */ - // solhint-disable-next-line no-empty-blocks - function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } -} diff --git a/packages/contracts-watr/contracts/common/ProxyStorage.sol b/packages/contracts-watr/contracts/common/ProxyStorage.sol index 5de13cd74..605fb761e 100644 --- a/packages/contracts-watr/contracts/common/ProxyStorage.sol +++ b/packages/contracts-watr/contracts/common/ProxyStorage.sol @@ -61,6 +61,9 @@ contract ProxyStorage { address public chainReserveFeed; bool public proofOfReserveEnabled; + // XC20Wrapper variables + address public nativeToken; + /* Additionally, we have several keccak-based storage locations. * If you add more keccak-based storage mappings, such as mappings, you must document them here. * If the length of the keccak input is the same as an existing mapping, it is possible there could be a preimage collision. diff --git a/packages/contracts-watr/contracts/common/ReclaimerToken.sol b/packages/contracts-watr/contracts/common/ReclaimerToken.sol index 48d603973..899e65b53 100644 --- a/packages/contracts-watr/contracts/common/ReclaimerToken.sol +++ b/packages/contracts-watr/contracts/common/ReclaimerToken.sol @@ -3,14 +3,14 @@ pragma solidity 0.6.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ITrueCurrency} from "../interface/ITrueCurrency.sol"; -import {ERC20} from "./ERC20.sol"; +import {XC20Wrapper} from "./XC20Wrapper.sol"; /** * @title ReclaimerToken * @dev ERC20 token which allows owner to reclaim ERC20 tokens * or ether sent to this contract */ -abstract contract ReclaimerToken is ERC20, ITrueCurrency { +abstract contract ReclaimerToken is XC20Wrapper, ITrueCurrency { /** * @dev send all eth balance in the contract to another address * @param _to address to send eth balance to diff --git a/packages/contracts-watr/contracts/common/XC20Wrapper.sol b/packages/contracts-watr/contracts/common/XC20Wrapper.sol new file mode 100644 index 000000000..2da1b7a70 --- /dev/null +++ b/packages/contracts-watr/contracts/common/XC20Wrapper.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.10; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Context} from "@openzeppelin/contracts/GSN/Context.sol"; +import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {ClaimableOwnable} from "./ClaimableOwnable.sol"; +import {IERC20Plus} from "../interface/IERC20Plus.sol"; + +abstract contract XC20Wrapper is IERC20, ClaimableOwnable, Context { + using SafeMath for uint256; + + function totalSupply() public view virtual override returns (uint256) { + return IERC20Plus(nativeToken).totalSupply(); + } + + function balanceOf(address account) external view virtual override returns (uint256) { + return IERC20Plus(nativeToken).balanceOf(account); + } + + function transfer(address recipient, uint256 amount) external virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + function allowance(address owner, address spender) external view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) external virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + function decreaseAllowance(address spender, uint256 amount) external virtual { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(amount, "XC20: decreased allowance below zero")); + } + + function increaseAllowance(address spender, uint256 amount) external virtual { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(amount)); + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external virtual override returns (bool) { + require(sender != address(0), "XC20: transfer from the zero address"); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "XC20: amount exceeds allowance")); + _transfer(sender, recipient, amount); + return true; + } + + function decimals() public view virtual returns (uint8) { + return IERC20Plus(nativeToken).decimals(); + } + + function name() public pure virtual returns (string memory); + + function symbol() public pure virtual returns (string memory); + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "XC20: mint to the zero address"); + IERC20Plus(nativeToken).mint(account, amount); + emit Transfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + IERC20Plus(nativeToken).burn(account, amount); + emit Transfer(account, address(0), amount); + } + + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + require(recipient != address(0), "XC20: transfer to the zero address"); + _forceTransfer(sender, recipient, amount); + emit Transfer(sender, recipient, amount); + } + + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0) && spender != address(0), "XC20: approve to the zero address"); + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + function _forceTransfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + require(IERC20Plus(nativeToken).balanceOf(sender) >= amount, "XC20: amount exceeds balance"); + IERC20Plus(nativeToken).burn(sender, amount); + IERC20Plus(nativeToken).mint(recipient, amount); + } +} diff --git a/packages/contracts-watr/contracts/interface/IERC20Plus.sol b/packages/contracts-watr/contracts/interface/IERC20Plus.sol new file mode 100644 index 000000000..01ab67ef1 --- /dev/null +++ b/packages/contracts-watr/contracts/interface/IERC20Plus.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.10; + +// IERC20 and IERC20Plus interfaces from +// https://github.com/AstarNetwork/astar-frame/blob/polkadot-v0.9.39/precompiles/assets-erc20/ERC20.sol + +interface IERC20 { + /** + * @dev Returns the name of the token. + * Selector: 06fdde03 + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + * Selector: 95d89b41 + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + * Selector: 313ce567 + */ + function decimals() external view returns (uint8); + + /** + * @dev Total number of tokens in existence + * Selector: 18160ddd + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Gets the balance of the specified address. + * Selector: 70a08231 + * @param who The address to query the balance of. + * @return An uint256 representing the amount owned by the passed address. + */ + function balanceOf(address who) external view returns (uint256); + + /** + * @dev Function to check the amount of tokens that an owner allowed to a spender. + * Selector: dd62ed3e + * @param owner address The address which owns the funds. + * @param spender address The address which will spend the funds. + * @return A uint256 specifying the amount of tokens still available for the spender. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Transfer token for a specified address + * Selector: a9059cbb + * @param to The address to transfer to. + * @param value The amount to be transferred. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Approve the passed address to spend the specified amount of tokens on behalf + * of msg.sender. + * Beware that changing an allowance with this method brings the risk that someone may + * use both the old + * and the new allowance by unfortunate transaction ordering. One possible solution to + * mitigate this race condition is to first reduce the spender's allowance to 0 and set + * the desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * Selector: 095ea7b3 + * @param spender The address which will spend the funds. + * @param value The amount of tokens to be spent. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Transfer tokens from one address to another + * Selector: 23b872dd + * @param from address The address which you want to send tokens from + * @param to address The address which you want to transfer to + * @param value uint256 the amount of tokens to be transferred + */ + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + /** + * @dev Event emited when a transfer has been performed. + * Selector: ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + * @param from address The address sending the tokens + * @param to address The address receiving the tokens. + * @param value uint256 The amount of tokens transfered. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Event emited when an approval has been registered. + * Selector: 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + * @param owner address Owner of the tokens. + * @param spender address Allowed spender. + * @param value uint256 Amount of tokens approved. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +/** + * @title Extension for ERC20 interface + * @dev Extended functions with minimum balance check as well as mint & burn. + */ +interface IERC20Plus is IERC20 { + /** + * @dev Returns minimum balance an account must have to exist + * Selector: b9d1d49b + */ + function minimumBalance() external view returns (uint256); + + /** + * @dev Mints the specified amount of asset for the beneficiary. + * This operation will increase the total supply. + * Only usable by asset admin. + * Selector: 40c10f19 + */ + function mint(address beneficiary, uint256 amount) external returns (bool); + + /** + * @dev Burns by up to the specified amount of asset from the target. + * This operation will increase decrease the total supply. + * Only usable by asset admin. + * Selector: 9dc29fac + */ + function burn(address who, uint256 amount) external returns (bool); +} diff --git a/packages/contracts-watr/contracts/mocks/MockTrueCurrency.sol b/packages/contracts-watr/contracts/mocks/MockTrueCurrency.sol index 4052b6fd5..c3b5ecc55 100644 --- a/packages/contracts-watr/contracts/mocks/MockTrueCurrency.sol +++ b/packages/contracts-watr/contracts/mocks/MockTrueCurrency.sol @@ -4,13 +4,8 @@ pragma solidity 0.6.10; import {TrueCurrencyWithProofOfReserve} from "../TrueCurrencyWithProofOfReserve.sol"; contract MockTrueCurrency is TrueCurrencyWithProofOfReserve { - uint8 constant DECIMALS = 18; uint8 constant ROUNDING = 2; - function decimals() public pure override returns (uint8) { - return DECIMALS; - } - function rounding() public pure returns (uint8) { return ROUNDING; } diff --git a/packages/contracts-watr/contracts/mocks/MockXC20.sol b/packages/contracts-watr/contracts/mocks/MockXC20.sol new file mode 100644 index 000000000..9d03d387c --- /dev/null +++ b/packages/contracts-watr/contracts/mocks/MockXC20.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.10; + +contract MockXC20 { + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + uint8 public decimals; + + constructor(uint8 _decimals) public { + decimals = _decimals; + } + + function mint(address account, uint256 amount) public returns (bool) { + balanceOf[account] += amount; + totalSupply += amount; + + return true; + } + + function burn(address account, uint256 amount) public returns (bool) { + require(balanceOf[account] >= amount, "XC20: amount exceeds balance"); + balanceOf[account] -= amount; + totalSupply -= amount; + + return true; + } +} diff --git a/packages/contracts-watr/contracts/mocks/TokenControllerMock.sol b/packages/contracts-watr/contracts/mocks/TokenControllerMock.sol index 15b94901d..5b1d84ac2 100644 --- a/packages/contracts-watr/contracts/mocks/TokenControllerMock.sol +++ b/packages/contracts-watr/contracts/mocks/TokenControllerMock.sol @@ -74,8 +74,6 @@ contract TokenControllerMock is TokenControllerV3 { } contract TokenControllerPauseMock is TokenControllerMock { - address public pausedImplementation; - function setPausedImplementation(address _pausedToken) external { pausedImplementation = _pausedToken; } diff --git a/packages/contracts-watr/contracts/paused/PausedTrueUSD.sol b/packages/contracts-watr/contracts/paused/PausedTrueUSD.sol new file mode 100644 index 000000000..b611dc411 --- /dev/null +++ b/packages/contracts-watr/contracts/paused/PausedTrueUSD.sol @@ -0,0 +1,999 @@ +// SPDX-License-Identifier: MIT + +// File: openzeppelin-solidity/contracts/token/ERC20/IERC20.sol + +pragma solidity 0.6.10; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +interface IERC20 { + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +// File: @trusttoken/registry/contracts/Registry.sol + +pragma solidity 0.6.10; + +interface RegistryClone { + function syncAttributeValue( + address _who, + bytes32 _attribute, + uint256 _value + ) external; +} + +contract Registry { + struct AttributeData { + uint256 value; + bytes32 notes; + address adminAddr; + uint256 timestamp; + } + + // never remove any storage variables + address public owner; + address public pendingOwner; + bool initialized; + + // Stores arbitrary attributes for users. An example use case is an IERC20 + // token that requires its users to go through a KYC/AML check - in this case + // a validator can set an account's "hasPassedKYC/AML" attribute to 1 to indicate + // that account can use the token. This mapping stores that value (1, in the + // example) as well as which validator last set the value and at what time, + // so that e.g. the check can be renewed at appropriate intervals. + mapping(address => mapping(bytes32 => AttributeData)) attributes; + // The logic governing who is allowed to set what attributes is abstracted as + // this accessManager, so that it may be replaced by the owner as needed + bytes32 constant WRITE_PERMISSION = keccak256("canWriteTo-"); + mapping(bytes32 => RegistryClone[]) subscribers; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event SetAttribute(address indexed who, bytes32 attribute, uint256 value, bytes32 notes, address indexed adminAddr); + event SetManager(address indexed oldManager, address indexed newManager); + event StartSubscription(bytes32 indexed attribute, RegistryClone indexed subscriber); + event StopSubscription(bytes32 indexed attribute, RegistryClone indexed subscriber); + + // Allows a write if either a) the writer is that Registry's owner, or + // b) the writer is writing to attribute foo and that writer already has + // the canWriteTo-foo attribute set (in that same Registry) + function confirmWrite(bytes32 _attribute, address _admin) internal view returns (bool) { + return (_admin == owner || hasAttribute(_admin, keccak256(abi.encodePacked(WRITE_PERMISSION ^ _attribute)))); + } + + // Writes are allowed only if the accessManager approves + function setAttribute( + address _who, + bytes32 _attribute, + uint256 _value, + bytes32 _notes + ) public { + require(confirmWrite(_attribute, msg.sender)); + attributes[_who][_attribute] = AttributeData(_value, _notes, msg.sender, block.timestamp); + emit SetAttribute(_who, _attribute, _value, _notes, msg.sender); + + RegistryClone[] storage targets = subscribers[_attribute]; + uint256 index = targets.length; + while (index-- > 0) { + targets[index].syncAttributeValue(_who, _attribute, _value); + } + } + + function subscribe(bytes32 _attribute, RegistryClone _syncer) external onlyOwner { + subscribers[_attribute].push(_syncer); + emit StartSubscription(_attribute, _syncer); + } + + function unsubscribe(bytes32 _attribute, uint256 _index) external onlyOwner { + uint256 length = subscribers[_attribute].length; + require(_index < length); + emit StopSubscription(_attribute, subscribers[_attribute][_index]); + subscribers[_attribute][_index] = subscribers[_attribute][length - 1]; + subscribers[_attribute].pop(); + } + + function subscriberCount(bytes32 _attribute) public view returns (uint256) { + return subscribers[_attribute].length; + } + + function setAttributeValue( + address _who, + bytes32 _attribute, + uint256 _value + ) public { + require(confirmWrite(_attribute, msg.sender)); + attributes[_who][_attribute] = AttributeData(_value, "", msg.sender, block.timestamp); + emit SetAttribute(_who, _attribute, _value, "", msg.sender); + RegistryClone[] storage targets = subscribers[_attribute]; + uint256 index = targets.length; + while (index-- > 0) { + targets[index].syncAttributeValue(_who, _attribute, _value); + } + } + + // Returns true if the uint256 value stored for this attribute is non-zero + function hasAttribute(address _who, bytes32 _attribute) public view returns (bool) { + return attributes[_who][_attribute].value != 0; + } + + // Returns the exact value of the attribute, as well as its metadata + function getAttribute(address _who, bytes32 _attribute) + public + view + returns ( + uint256, + bytes32, + address, + uint256 + ) + { + AttributeData memory data = attributes[_who][_attribute]; + return (data.value, data.notes, data.adminAddr, data.timestamp); + } + + function getAttributeValue(address _who, bytes32 _attribute) public view returns (uint256) { + return attributes[_who][_attribute].value; + } + + function getAttributeAdminAddr(address _who, bytes32 _attribute) public view returns (address) { + return attributes[_who][_attribute].adminAddr; + } + + function getAttributeTimestamp(address _who, bytes32 _attribute) public view returns (uint256) { + return attributes[_who][_attribute].timestamp; + } + + function syncAttribute( + bytes32 _attribute, + uint256 _startIndex, + address[] calldata _addresses + ) external { + RegistryClone[] storage targets = subscribers[_attribute]; + uint256 index = targets.length; + while (index-- > _startIndex) { + RegistryClone target = targets[index]; + for (uint256 i = _addresses.length; i-- > 0; ) { + address who = _addresses[i]; + target.syncAttributeValue(who, _attribute, attributes[who][_attribute].value); + } + } + } + + function reclaimEther(address payable _to) external onlyOwner { + _to.transfer(address(this).balance); + } + + function reclaimToken(IERC20 token, address _to) external onlyOwner { + uint256 balance = token.balanceOf(address(this)); + token.transfer(_to, balance); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner, "only Owner"); + _; + } + + /** + * @dev Modifier throws if called by any account other than the pendingOwner. + */ + modifier onlyPendingOwner() { + require(msg.sender == pendingOwner); + _; + } + + /** + * @dev Allows the current owner to set the pendingOwner address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + } + + /** + * @dev Allows the pendingOwner address to finalize the transfer. + */ + function claimOwnership() public onlyPendingOwner { + emit OwnershipTransferred(owner, pendingOwner); + owner = pendingOwner; + pendingOwner = address(0); + } +} + +// File: contracts/TrueCurrencies/modularERC20/InstantiatableOwnable.sol + +pragma solidity 0.6.10; + +/** + * @title InstantiatableOwnable + * @dev The InstantiatableOwnable contract has an owner address, and provides basic authorization control + * functions, this simplifies the implementation of "user permissions". + */ +contract InstantiatableOwnable { + address public owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev The InstantiatableOwnable constructor sets the original `owner` of the contract to the sender + * account. + */ + constructor() public { + owner = msg.sender; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + /** + * @dev Allows the current owner to transfer control of the contract to a newOwner. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0)); + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } +} + +// File: contracts/TrueCurrencies/modularERC20/Claimable.sol + +pragma solidity 0.6.10; + +/** + * @title Claimable + * @dev Extension for the InstantiatableOwnable contract, where the ownership needs to be claimed. + * This allows the new owner to accept the transfer. + */ +contract Claimable is InstantiatableOwnable { + address public pendingOwner; + + /** + * @dev Modifier throws if called by any account other than the pendingOwner. + */ + modifier onlyPendingOwner() { + require(msg.sender == pendingOwner); + _; + } + + /** + * @dev Allows the current owner to set the pendingOwner address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public override onlyOwner { + pendingOwner = newOwner; + } + + /** + * @dev Allows the pendingOwner address to finalize the transfer. + */ + function claimOwnership() public onlyPendingOwner { + emit OwnershipTransferred(owner, pendingOwner); + owner = pendingOwner; + pendingOwner = address(0); + } +} + +// File: openzeppelin-solidity/contracts/math/SafeMath.sol + +pragma solidity 0.6.10; + +/** + * @dev Wrappers over Solidity's arithmetic operations with added overflow + * checks. + * + * Arithmetic operations in Solidity wrap on overflow. This can easily result + * in bugs, because programmers usually assume that an overflow raises an + * error, which is the standard behavior in high level programming languages. + * `SafeMath` restores this intuition by reverting the transaction when an + * operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "SafeMath: addition overflow"); + + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, "SafeMath: subtraction overflow"); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + * + * _Available since v2.4.0._ + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b, "SafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, "SafeMath: division by zero"); + } + + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold + + return c; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, "SafeMath: modulo by zero"); + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + * + * _Available since v2.4.0._ + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } +} + +// File: contracts/TrueCurrencies/modularERC20/BalanceSheet.sol + +pragma solidity 0.6.10; + +// A wrapper around the balanceOf mapping. +contract BalanceSheet is Claimable { + using SafeMath for uint256; + + mapping(address => uint256) public balanceOf; + + function addBalance(address _addr, uint256 _value) public onlyOwner { + balanceOf[_addr] = balanceOf[_addr].add(_value); + } + + function subBalance(address _addr, uint256 _value) public onlyOwner { + balanceOf[_addr] = balanceOf[_addr].sub(_value); + } + + function setBalance(address _addr, uint256 _value) public onlyOwner { + balanceOf[_addr] = _value; + } +} + +// File: contracts/TrueCurrencies/modularERC20/AllowanceSheet.sol + +pragma solidity 0.6.10; + +// A wrapper around the allowanceOf mapping. +contract AllowanceSheet is Claimable { + using SafeMath for uint256; + + mapping(address => mapping(address => uint256)) public allowanceOf; + + function addAllowance( + address _tokenHolder, + address _spender, + uint256 _value + ) public onlyOwner { + allowanceOf[_tokenHolder][_spender] = allowanceOf[_tokenHolder][_spender].add(_value); + } + + function subAllowance( + address _tokenHolder, + address _spender, + uint256 _value + ) public onlyOwner { + allowanceOf[_tokenHolder][_spender] = allowanceOf[_tokenHolder][_spender].sub(_value); + } + + function setAllowance( + address _tokenHolder, + address _spender, + uint256 _value + ) public onlyOwner { + allowanceOf[_tokenHolder][_spender] = _value; + } +} + +// File: contracts/TrueReward/FinancialOpportunity.sol + +pragma solidity 0.6.10; + +/** + * @title FinancialOpportunity + * @dev Interface for third parties to implement financial opportunities + * + * -- Overview -- + * The goal of this contract is to allow anyone to create an opportunity + * to earn interest on TUSD. deposit() "mints" yTUSD whcih is redeemable + * for some amount of TUSD. TrueUSD wraps this contractwith TrustToken + * Assurance, which provides protection from bugs and system design flaws + * TUSD is a compliant stablecoin, therefore we do not allow transfers of + * yTUSD, thus there are no transfer functions + * + * -- tokenValue() -- + * This function returns the value in TUSD of 1 yTUSD + * This value should never decrease + * + * -- TUSD vs yTUSD -- + * yTUSD represents a fixed value which is redeemable for some amount of TUSD + * Think of yTUSD like cTUSD, where cTokens are minted and increase in value versus + * the underlying asset as interest is accrued + * + * -- totalSupply() -- + * This function returns the total supply of yTUSD issued by this contract + * It is important to track this value accuratley and add/deduct the correct + * amount on deposit/redemptions + * + * -- Assumptions -- + * - tokenValue can never decrease + * - total TUSD owed to depositors = tokenValue() * totalSupply() + */ +interface FinancialOpportunity { + /** + * @dev Returns total supply of yTUSD in this contract + * + * @return total supply of yTUSD in this contract + **/ + function totalSupply() external view returns (uint256); + + /** + * @dev Exchange rate between TUSD and yTUSD + * + * tokenValue should never decrease + * + * @return TUSD / yTUSD price ratio + */ + function tokenValue() external view returns (uint256); + + /** + * @dev deposits TrueUSD and returns yTUSD minted + * + * We can think of deposit as a minting function which + * will increase totalSupply of yTUSD based on the deposit + * + * @param from account to transferFrom + * @param amount amount in TUSD to deposit + * @return yTUSD minted from this deposit + */ + function deposit(address from, uint256 amount) external returns (uint256); + + /** + * @dev Redeem yTUSD for TUSD and withdraw to account + * + * This function should use tokenValue to calculate + * how much TUSD is owed. This function should burn yTUSD + * after redemption + * + * This function must return value in TUSD + * + * @param to account to transfer TUSD for + * @param amount amount in TUSD to withdraw from finOp + * @return TUSD amount returned from this transaction + */ + function redeem(address to, uint256 amount) external returns (uint256); +} + +// File: contracts/TrueCurrencies/ProxyStorage.sol + +pragma solidity 0.6.10; + +/* +Defines the storage layout of the token implementation contract. Any newly declared +state variables in future upgrades should be appended to the bottom. Never remove state variables +from this list + */ +contract ProxyStorage { + address public owner; + address public pendingOwner; + + bool initialized; + + BalanceSheet balances_Deprecated; + AllowanceSheet allowances_Deprecated; + + uint256 totalSupply_; + + bool private paused_Deprecated = false; + address private globalPause_Deprecated; + + uint256 public burnMin = 0; + uint256 public burnMax = 0; + + Registry public registry; + + string name_Deprecated; + string symbol_Deprecated; + + uint256[] gasRefundPool_Deprecated; + uint256 private redemptionAddressCount_Deprecated; + uint256 public minimumGasPriceForFutureRefunds; + + mapping(address => uint256) _balanceOf; + mapping(address => mapping(address => uint256)) _allowance; + mapping(bytes32 => mapping(address => uint256)) attributes; + + // reward token storage + mapping(address => FinancialOpportunity) finOps; + mapping(address => mapping(address => uint256)) finOpBalances; + mapping(address => uint256) finOpSupply; + + // true reward allocation + // proportion: 1000 = 100% + struct RewardAllocation { + uint256 proportion; + address finOp; + } + mapping(address => RewardAllocation[]) _rewardDistribution; + uint256 maxRewardProportion = 1000; + + /* Additionally, we have several keccak-based storage locations. + * If you add more keccak-based storage mappings, such as mappings, you must document them here. + * If the length of the keccak input is the same as an existing mapping, it is possible there could be a preimage collision. + * A preimage collision can be used to attack the contract by treating one storage location as another, + * which would always be a critical issue. + * Carefully examine future keccak-based storage to ensure there can be no preimage collisions. + ******************************************************************************************************* + ** length input usage + ******************************************************************************************************* + ** 19 "trueXXX.proxy.owner" Proxy Owner + ** 27 "trueXXX.pending.proxy.owner" Pending Proxy Owner + ** 28 "trueXXX.proxy.implementation" Proxy Implementation + ** 32 uint256(11) gasRefundPool_Deprecated + ** 64 uint256(address),uint256(14) balanceOf + ** 64 uint256(address),keccak256(uint256(address),uint256(15)) allowance + ** 64 uint256(address),keccak256(bytes32,uint256(16)) attributes + **/ +} + +// File: contracts/TrueCurrencies/HasOwner.sol + +pragma solidity 0.6.10; + +/** + * @title HasOwner + * @dev The HasOwner contract is a copy of Claimable Contract by Zeppelin. + and provides basic authorization control functions. Inherits storage layout of + ProxyStorage. + */ +contract HasOwner is ProxyStorage { + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev sets the original `owner` of the contract to the sender + * at construction. Must then be reinitialized + */ + constructor() public { + owner = msg.sender; + emit OwnershipTransferred(address(0), owner); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner, "only Owner"); + _; + } + + /** + * @dev Modifier throws if called by any account other than the pendingOwner. + */ + modifier onlyPendingOwner() { + require(msg.sender == pendingOwner); + _; + } + + /** + * @dev Allows the current owner to set the pendingOwner address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + pendingOwner = newOwner; + } + + /** + * @dev Allows the pendingOwner address to finalize the transfer. + */ + function claimOwnership() public onlyPendingOwner { + emit OwnershipTransferred(owner, pendingOwner); + owner = pendingOwner; + pendingOwner = address(0); + } +} + +// File: contracts/TrueCurrencies/utilities/PausedToken.sol + +pragma solidity 0.6.10; + +contract PausedToken is HasOwner, RegistryClone { + using SafeMath for uint256; + event Transfer(address indexed from, address indexed to, uint256 value); + event AllowanceSheetSet(address indexed sheet); + event BalanceSheetSet(address indexed sheet); + uint8 constant DECIMALS = 18; + uint8 constant ROUNDING = 2; + + event WipeBlacklistedAccount(address indexed account, uint256 balance); + event SetRegistry(address indexed registry); + + function decimals() public pure returns (uint8) { + return DECIMALS; + } + + function rounding() public pure returns (uint8) { + return ROUNDING; + } + + /** + *@dev send all eth balance in the TrueUSD contract to another address + */ + function reclaimEther(address payable _to) external onlyOwner { + _to.transfer(address(this).balance); + } + + /** + *@dev send all token balance of an arbitary erc20 token + in the TrueUSD contract to another address + */ + function reclaimToken(IERC20 token, address _to) external onlyOwner { + uint256 balance = token.balanceOf(address(this)); + token.transfer(_to, balance); + } + + /** + *@dev allows owner of TrueUSD to gain ownership of any contract that TrueUSD currently owns + */ + function reclaimContract(InstantiatableOwnable _ownable) external onlyOwner { + _ownable.transferOwnership(owner); + } + + function totalSupply() public view returns (uint256) { + return totalSupply_; + } + + /** + *@dev Return the remaining sponsored gas slots + */ + function remainingGasRefundPool() public view returns (uint256 length) { + assembly { + length := sload(0xfffff) + } + } + + function sponsorGas() external { + uint256 refundPrice = minimumGasPriceForFutureRefunds; + require(refundPrice > 0); + assembly { + let offset := sload(0xfffff) + let result := add(offset, 9) + sstore(0xfffff, result) + let position := add(offset, 0x100000) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + position := add(position, 1) + sstore(position, refundPrice) + } + } + + bytes32 constant CAN_SET_FUTURE_REFUND_MIN_GAS_PRICE = "canSetFutureRefundMinGasPrice"; + + function setMinimumGasPriceForFutureRefunds(uint256 _minimumGasPriceForFutureRefunds) public { + require(registry.hasAttribute(msg.sender, CAN_SET_FUTURE_REFUND_MIN_GAS_PRICE)); + minimumGasPriceForFutureRefunds = _minimumGasPriceForFutureRefunds; + } + + function balanceOf(address _who) public view returns (uint256) { + return _getBalance(_who); + } + + function _getBalance(address _who) internal view returns (uint256 value) { + return _balanceOf[_who]; + } + + function _setBalance(address _who, uint256 _value) internal { + _balanceOf[_who] = _value; + } + + function allowance(address _who, address _spender) public view returns (uint256) { + return _getAllowance(_who, _spender); + } + + function _getAllowance(address _who, address _spender) internal view returns (uint256 value) { + return _allowance[_who][_spender]; + } + + function transfer( + address, /*_to*/ + uint256 /*_value*/ + ) public pure returns (bool) { + revert("Token Paused"); + } + + function transferFrom( + address, /*_from*/ + address, /*_to*/ + uint256 /*_value*/ + ) public pure returns (bool) { + revert("Token Paused"); + } + + function burn( + uint256 /*_value*/ + ) public pure { + revert("Token Paused"); + } + + function mint( + address, /*_to*/ + uint256 /*_value*/ + ) public view onlyOwner { + revert("Token Paused"); + } + + function approve( + address, /*_spender*/ + uint256 /*_value*/ + ) public pure returns (bool) { + revert("Token Paused"); + } + + function increaseAllowance( + address, /*_spender*/ + uint256 /*_addedValue*/ + ) public pure returns (bool) { + revert("Token Paused"); + } + + function decreaseAllowance( + address, /*_spender*/ + uint256 /*_subtractedValue*/ + ) public pure returns (bool) { + revert("Token Paused"); + } + + function paused() public pure returns (bool) { + return true; + } + + function setRegistry(Registry _registry) public onlyOwner { + registry = _registry; + emit SetRegistry(address(registry)); + } + + modifier onlyRegistry() { + require(msg.sender == address(registry)); + _; + } + + function syncAttributeValue( + address _who, + bytes32 _attribute, + uint256 _value + ) public override onlyRegistry { + attributes[_attribute][_who] = _value; + } + + bytes32 constant IS_BLACKLISTED = "isBlacklisted"; + + function wipeBlacklistedAccount(address _account) public onlyOwner { + require(attributes[IS_BLACKLISTED][_account] != 0, "_account is not blacklisted"); + uint256 oldValue = _getBalance(_account); + _setBalance(_account, 0); + totalSupply_ = totalSupply_.sub(oldValue); + emit WipeBlacklistedAccount(_account, oldValue); + emit Transfer(_account, address(0), oldValue); + } +} + +pragma solidity 0.6.10; + +contract PausedTrueUSD is PausedToken { + function name() public pure returns (string memory) { + return "TrueUSD"; + } + + function symbol() public pure returns (string memory) { + return "TUSD"; + } +} diff --git a/packages/contracts-watr/contracts/tokens/TrueUSD.sol b/packages/contracts-watr/contracts/tokens/TrueUSD.sol index bd67a72eb..d14d18373 100644 --- a/packages/contracts-watr/contracts/tokens/TrueUSD.sol +++ b/packages/contracts-watr/contracts/tokens/TrueUSD.sol @@ -9,13 +9,8 @@ import {TrueCurrencyWithProofOfReserve} from "../TrueCurrencyWithProofOfReserve. * inherited - see the documentation on the corresponding contracts. */ contract TrueUSD is TrueCurrencyWithProofOfReserve { - uint8 constant DECIMALS = 18; uint8 constant ROUNDING = 2; - function decimals() public pure override returns (uint8) { - return DECIMALS; - } - function rounding() public pure returns (uint8) { return ROUNDING; } diff --git a/packages/contracts-watr/package.json b/packages/contracts-watr/package.json index d83d3f4d7..390232909 100644 --- a/packages/contracts-watr/package.json +++ b/packages/contracts-watr/package.json @@ -17,7 +17,8 @@ "build": "yarn build:hardhat && yarn build:typechain && mars && yarn build:canary", "preflatten": "rm -rf custom_flatten", "flatten": "waffle flatten .waffle.json", - "test": "mocha 'test/**/*.test.ts'", + "test": "mocha 'test/{,!(verifyDeployment)}/*.test.ts'", + "test:deployments": "dotenv -e .env.test -- mocha 'test/verifyDeployment/*.test.ts'", "checks": "yarn lint && yarn test", "deploy": "./utils/bash/marsDeploy.sh scripts/deployment/deploy.ts" }, @@ -46,6 +47,8 @@ "typescript": "4.5.4", "solhint": "^3.0.0", "prettylint": "^1.0.0", - "prettier": "^2.4.1" + "prettier": "^2.4.1", + "dotenv": "^16.0.3", + "dotenv-cli": "^7.0.0" } } diff --git a/packages/contracts-watr/scripts/deployment/baseDeployment.ts b/packages/contracts-watr/scripts/deployment/baseDeployment.ts index 481c1b3cd..979f754a6 100644 --- a/packages/contracts-watr/scripts/deployment/baseDeployment.ts +++ b/packages/contracts-watr/scripts/deployment/baseDeployment.ts @@ -1,15 +1,25 @@ -import { TrueUSD } from '../../build/artifacts' +import { PausedTrueUSD, TrueUSD } from '../../build/artifacts' import { deployToken } from './deployToken' -import { deployTokenController, setupTokenController } from './deployTokenController' +import { + deployTokenController, + setupMintThresholds, setupTokenController, +} from './deployTokenController' import { deployRegistry } from './deployRegistry' +import { contract } from 'ethereum-mars' export function baseDeployment() { + const tokenId = parseInt(process.env['TRUE_USD_ASSET_ID']) + if (isNaN(tokenId)) { + throw new Error('TRUE_USD_ASSET_ID not provided or invalid') + } + + const pausedImplementation = contract(PausedTrueUSD) const { implementation: tokenControllerImplementation, proxy: tokenControllerProxy, - } = deployTokenController() + } = deployTokenController(pausedImplementation) - const { implementation: trueUSDImplementation, proxy: trueUSDProxy } = deployToken(TrueUSD, tokenControllerProxy) + const { implementation: trueUSDImplementation, proxy: trueUSDProxy } = deployToken(TrueUSD, tokenControllerProxy, tokenId) const { implementation: registryImplementation, @@ -17,6 +27,7 @@ export function baseDeployment() { } = deployRegistry() setupTokenController(tokenControllerProxy, trueUSDProxy, registryProxy) + setupMintThresholds(tokenControllerProxy) return { trueUSDImplementation, diff --git a/packages/contracts-watr/scripts/deployment/deployToken.ts b/packages/contracts-watr/scripts/deployment/deployToken.ts index 5f9dcc69e..b4da3f5d1 100644 --- a/packages/contracts-watr/scripts/deployment/deployToken.ts +++ b/packages/contracts-watr/scripts/deployment/deployToken.ts @@ -1,20 +1,21 @@ import { AddressLike, ArtifactFrom, contract, createProxy, Transaction, TransactionOverrides } from 'ethereum-mars' import { OwnedUpgradeabilityProxy } from '../../build/artifacts' import { NoParams } from 'ethereum-mars/build/src/syntax/contract' +import { utils } from 'ethers' type Token = NoParams & { - initialize(options?: TransactionOverrides): Transaction, + initialize(nativeToken: AddressLike, options?: TransactionOverrides): Transaction, transferOwnership(newOwner: AddressLike, options?: TransactionOverrides): Transaction, } -export function deployToken(tokenArtifact: ArtifactFrom, controller: AddressLike) { +export function deployToken(tokenArtifact: ArtifactFrom, controller: AddressLike, assetId: number) { const implementation = contract(tokenArtifact) const tokenProxy = createProxy(OwnedUpgradeabilityProxy, (proxy) => { proxy.upgradeTo(implementation) proxy.transferProxyOwnership(controller) }) const proxy = tokenProxy(implementation, (token) => { - token.initialize() + token.initialize(generatePrecompileAddress(assetId)) token.transferOwnership(controller) }) @@ -23,3 +24,8 @@ export function deployToken(tokenArtifact: ArtifactFrom, con proxy, } } + +function generatePrecompileAddress(assetId: number) { + const idHex = assetId.toString(16) + return utils.getAddress('0xffffffff' + Array.from({ length: 32 - idHex.length }, () => '0').join('') + idHex) +} diff --git a/packages/contracts-watr/scripts/deployment/deployTokenController.ts b/packages/contracts-watr/scripts/deployment/deployTokenController.ts index 1cd6f7747..165fc64ae 100644 --- a/packages/contracts-watr/scripts/deployment/deployTokenController.ts +++ b/packages/contracts-watr/scripts/deployment/deployTokenController.ts @@ -1,12 +1,16 @@ import { AddressLike, contract, createProxy } from 'ethereum-mars' -import { OwnedUpgradeabilityProxy, TokenControllerV3 } from '../../build/artifacts' +import { + OwnedUpgradeabilityProxy, + TokenControllerV3, +} from '../../build/artifacts' +import { parseEther } from '@ethersproject/units' -export function deployTokenController() { +export function deployTokenController(pausedImplementation: AddressLike) { const ownedUpgradabilityProxy = createProxy(OwnedUpgradeabilityProxy) const implementation = contract(TokenControllerV3) const proxy = ownedUpgradabilityProxy(implementation, (controller) => { - controller.initialize() + controller.initialize(pausedImplementation) }) return { @@ -21,3 +25,12 @@ export function setupTokenController(controller: ReturnType['proxy']) { + controller.setMintThresholds(parseEther('1000000'), parseEther('5000000'), parseEther('10000000')) + controller.setMintLimits(parseEther('10000000'), parseEther('50000000'), parseEther('100000000')) + controller.refillMultiSigMintPool() + controller.refillRatifiedMintPool() + controller.refillInstantMintPool() + controller.setBurnBounds(parseEther('1000'), parseEther('1000000000')) +} diff --git a/packages/contracts-watr/test/ERC20.test.ts b/packages/contracts-watr/test/ERC20.test.ts new file mode 100644 index 000000000..8446b3787 --- /dev/null +++ b/packages/contracts-watr/test/ERC20.test.ts @@ -0,0 +1,451 @@ +import { expect, use } from 'chai' +import { solidity } from 'ethereum-waffle' +import { beforeEachWithFixture } from 'fixtures/beforeEachWithFixture' +import { BigNumber, BigNumberish, constants, Wallet } from 'ethers' +import { MockXC20__factory, TrueUSD, TrueUSD__factory } from 'contracts' +import { parseTrueUSD, trueUSDDecimals } from 'utils' + +use(solidity) + +describe('TrueCurrency - ERC20', () => { + let owner: Wallet + let initialHolder: Wallet + let secondAccount: Wallet + let thirdAccount: Wallet + let token: TrueUSD + + const initialSupply = parseTrueUSD('1000') + + beforeEachWithFixture(async (wallets) => { + [owner, initialHolder, secondAccount, thirdAccount] = wallets + const mockXC20 = await new MockXC20__factory(owner).deploy(trueUSDDecimals) + token = await new TrueUSD__factory(owner).deploy() + await token.initialize(mockXC20.address) + await token.connect(owner).mint(initialHolder.address, initialSupply) + }) + + function approve(tokenOwner: Wallet, spender: { address: string }, amount: BigNumberish) { + const asTokenOwner = token.connect(tokenOwner) + return asTokenOwner.approve(spender.address, amount) + } + + describe('totalSupply', () => { + it('returns the total amount of tokens', async () => { + const totalSupply = await token.totalSupply() + + expect(totalSupply).to.eq(parseTrueUSD('1000')) + }) + }) + + describe('balanceOf', () => { + describe('when the requested account has no tokens', () => { + it('returns zero', async () => { + expect(await token.balanceOf(secondAccount.address)).to.eq(0) + }) + }) + + describe('when the requested account has some tokens', () => { + it('returns the total amount of tokens', async () => { + expect(await token.balanceOf(initialHolder.address)).to.eq(initialSupply) + }) + }) + }) + + describe('transfer', () => { + function transfer(sender: Wallet, recipient: { address: string }, amount: BigNumberish) { + const asSender = token.connect(sender) + return asSender.transfer(recipient.address, amount) + } + + describe('when the recipient is not the zero address', () => { + describe('when the sender does not have enough balance', () => { + it('reverts', async () => { + await expect(transfer(secondAccount, thirdAccount, 100)) + .to.be.revertedWith('XC20: amount exceeds balance') + }) + }) + + describe('when the sender transfers all balance', () => { + let from: Wallet + let to: Wallet + const amount = initialSupply + + beforeEach(() => { + from = initialHolder + to = secondAccount + }) + + it('transfers the requested amount', async () => { + await transfer(from, to, amount) + + expect(await token.balanceOf(from.address)).to.eq(0) + expect(await token.balanceOf(to.address)).to.eq(amount) + }) + + it('emits a transfer event', async () => { + await expect(transfer(from, to, amount)) + .to.emit(token, 'Transfer') + .withArgs(from.address, to.address, amount) + }) + }) + + describe('when the sender transfers zero tokens', () => { + let from: Wallet + let to: Wallet + const amount = 0 + + beforeEach(() => { + from = initialHolder + to = secondAccount + }) + + it('transfers the requested amount', async () => { + await transfer(from, to, amount) + + expect(await token.balanceOf(from.address)).to.eq(initialSupply) + expect(await token.balanceOf(to.address)).to.eq(0) + }) + + it('emits a transfer event', async () => { + await expect(transfer(from, to, amount)) + .to.emit(token, 'Transfer') + .withArgs(from.address, to.address, amount) + }) + }) + }) + + describe('when the recipient is the zero address', () => { + it('reverts', async () => { + await expect(transfer(initialHolder, { address: constants.AddressZero }, initialSupply)) + .to.be.revertedWith('XC20: transfer to the zero address') + }) + }) + }) + + describe('transferFrom', () => { + function transferFrom( + spender: Wallet, + tokenOwner: { address: string }, + recipient: { address: string }, + amount: BigNumberish, + ) { + const asSpender = token.connect(spender) + return asSpender.transferFrom(tokenOwner.address, recipient.address, amount) + } + + let spender: Wallet + + beforeEach(() => { + spender = secondAccount + }) + + describe('when the token owner is not the zero address', () => { + let tokenOwner: Wallet + + beforeEach(() => { + tokenOwner = initialHolder + }) + + describe('when the recipient is not the zero address', () => { + let recipient: Wallet + + beforeEach(() => { + recipient = thirdAccount + }) + + describe('when the spender has enough approved balance', () => { + beforeEach(async () => { + await approve(tokenOwner, spender, initialSupply) + }) + + describe('when the token owner has enough balance', () => { + const amount = initialSupply + + it('transfers the requested amount', async () => { + try { + await transferFrom(spender, tokenOwner, recipient, amount) + } catch (e) { + console.log(e) + } + + expect(await token.balanceOf(tokenOwner.address)).to.eq(0) + expect(await token.balanceOf(recipient.address)).to.eq(amount) + }) + + it('decreases the spender allowance', async () => { + await transferFrom(spender, tokenOwner, recipient, amount) + + expect(await token.allowance(tokenOwner.address, spender.address)).to.eq(0) + }) + + it('emits a transfer event', async () => { + await expect(transferFrom(spender, tokenOwner, recipient, amount)) + .to.emit(token, 'Transfer') + .withArgs(tokenOwner.address, recipient.address, amount) + }) + + it('emits an approval event', async () => { + await expect(transferFrom(spender, tokenOwner, recipient, amount)) + .to.emit(token, 'Approval') + .withArgs(tokenOwner.address, spender.address, 0) + }) + }) + + describe('when the token owner does not have enough balance', () => { + const amount = initialSupply.add(1) + + it('reverts', async () => { + await expect(transferFrom(spender, tokenOwner, recipient, amount)) + .to.be.revertedWith('XC20: amount exceeds balance') + }) + }) + }) + + describe('when the spender does not have enough approved balance', () => { + beforeEach(async () => { + await approve(tokenOwner, spender, initialSupply.sub(1)) + }) + + describe('when the token owner has enough balance', () => { + const amount = initialSupply + + it('reverts', async () => { + await expect(transferFrom(spender, tokenOwner, recipient, amount)) + .to.be.revertedWith('XC20: amount exceeds allowance') + }) + }) + + describe('when the token owner does not have enough balance', () => { + const amount = initialSupply.add(1) + + it('reverts', async () => { + await expect(transferFrom(spender, tokenOwner, recipient, amount)) + .to.be.revertedWith('XC20: amount exceeds balance') + }) + }) + }) + }) + + describe('when the recipient is the zero address', () => { + const recipient = { address: constants.AddressZero } + const amount = initialSupply + + beforeEach(async () => { + await approve(tokenOwner, spender, amount) + }) + + it('reverts', async () => { + await expect(transferFrom(spender, tokenOwner, recipient, amount)) + .to.be.revertedWith('XC20: transfer to the zero address') + }) + }) + }) + + describe('when the token owner is the zero address', () => { + const tokenOwner = { address: constants.AddressZero } + + it('reverts', async () => { + await expect(transferFrom(spender, tokenOwner, thirdAccount, 0)) + .to.be.revertedWith('XC20: transfer from the zero address') + }) + }) + }) + + describe('approve', () => { + let tokenOwner: Wallet + + beforeEach(() => { + tokenOwner = initialHolder + }) + + describe('when the spender is not the zero address', () => { + let spender: Wallet + + beforeEach(() => { + spender = secondAccount + }) + + function describeApprove(description: string, amount: BigNumberish) { + describe(description, () => { + it('emits an approval event', async () => { + await expect(approve(tokenOwner, spender, amount)) + .to.emit(token, 'Approval') + .withArgs(tokenOwner.address, spender.address, amount) + }) + + describe('when there was no approved amount before', () => { + it('approves the requested amount', async () => { + await approve(tokenOwner, spender, amount) + expect(await token.allowance(tokenOwner.address, spender.address)).to.eq(amount) + }) + }) + + describe('when the spender had an approved amount', () => { + beforeEach(async () => { + await approve(tokenOwner, spender, 1) + }) + + it('approves the requested amount and replaces the previous one', async () => { + await approve(tokenOwner, spender, amount) + expect(await token.allowance(tokenOwner.address, spender.address)).to.eq(amount) + }) + }) + }) + } + + describeApprove('when the sender has enough balance', initialSupply) + describeApprove('when the sender does not have enough balance', initialSupply.add(1)) + }) + + describe('when the spender is the zero address', () => { + const spender = { address: constants.AddressZero } + + it('reverts', async () => { + await expect(approve(tokenOwner, spender, initialSupply)) + .to.be.revertedWith('XC20: approve to the zero address') + }) + }) + }) + + describe('decreaseAllowance', () => { + function decreaseAllowance(tokenOwner: Wallet, spender: { address: string }, subtractedValue: BigNumberish) { + const asTokenOwner = token.connect(tokenOwner) + return asTokenOwner.decreaseAllowance(spender.address, subtractedValue) + } + + let tokenOwner: Wallet + + beforeEach(() => { + tokenOwner = initialHolder + }) + + describe('when the spender is not the zero address', () => { + let spender: Wallet + + beforeEach(() => { + spender = secondAccount + }) + + function shouldDecreaseApproval(amount: BigNumber) { + describe('when there was no approved amount before', () => { + it('reverts', async () => { + await expect(decreaseAllowance(tokenOwner, spender, amount)) + .to.be.revertedWith('XC20: decreased allowance below zero') + }) + }) + + describe('when the spender had an approved amount', () => { + const approvedAmount = amount + + beforeEach(async () => { + await approve(tokenOwner, spender, approvedAmount) + }) + + it('emits an approval event', async () => { + await expect(decreaseAllowance(tokenOwner, spender, approvedAmount)) + .to.emit(token, 'Approval') + .withArgs(tokenOwner.address, spender.address, 0) + }) + + it('decreases the spender allowance subtracting the requested amount', async () => { + await decreaseAllowance(tokenOwner, spender, approvedAmount.sub(1)) + expect(await token.allowance(tokenOwner.address, spender.address)).to.eq(1) + }) + + it('sets the allowance to zero when all allowance is removed', async () => { + await decreaseAllowance(tokenOwner, spender, approvedAmount) + expect(await token.allowance(tokenOwner.address, spender.address)).to.eq(0) + }) + + it('reverts when more than the full allowance is removed', async () => { + await expect(decreaseAllowance(tokenOwner, spender, approvedAmount.add(1))) + .to.be.revertedWith('XC20: decreased allowance below zero') + }) + }) + } + + describe('when the sender has enough balance', () => { + shouldDecreaseApproval(initialSupply) + }) + + describe('when the sender does not have enough balance', () => { + shouldDecreaseApproval(initialSupply.add(1)) + }) + }) + + describe('when the spender is the zero address', () => { + const spender = { address: constants.AddressZero } + const amount = initialSupply + + it('reverts', async () => { + await expect(decreaseAllowance(tokenOwner, spender, amount)) + .to.be.revertedWith('XC20: decreased allowance below zero') + }) + }) + }) + + describe('increaseAllowance', () => { + function increaseAllowance(tokenOwner: Wallet, spender: { address: string }, addedValue: BigNumberish) { + const asTokenOwner = token.connect(tokenOwner) + return asTokenOwner.increaseAllowance(spender.address, addedValue) + } + + let tokenOwner: Wallet + + beforeEach(() => { + tokenOwner = initialHolder + }) + + describe('when the spender is not the zero address', () => { + let spender: Wallet + + beforeEach(() => { + spender = secondAccount + }) + + function shouldIncreaseApproval(amount: BigNumber) { + it('emits an approval event', async () => { + await expect(increaseAllowance(tokenOwner, spender, amount)) + .to.emit(token, 'Approval') + .withArgs(tokenOwner.address, spender.address, amount) + }) + + describe('when there was no approved amount before', () => { + it('approves the requested amount', async () => { + await increaseAllowance(tokenOwner, spender, amount) + expect(await token.allowance(tokenOwner.address, spender.address)).to.eq(amount) + }) + }) + + describe('when the spender had an approved amount', () => { + beforeEach(async () => { + await approve(tokenOwner, spender, 1) + }) + + it('increases the spender allowance adding the requested amount', async () => { + await increaseAllowance(tokenOwner, spender, amount) + expect(await token.allowance(tokenOwner.address, spender.address)).to.eq(amount.add(1)) + }) + }) + } + + describe('when the sender has enough balance', () => { + shouldIncreaseApproval(initialSupply) + }) + + describe('when the sender does not have enough balance', () => { + shouldIncreaseApproval(initialSupply.add(1)) + }) + }) + + describe('when the spender is the zero address', () => { + const spender = { address: constants.AddressZero } + const amount = initialSupply + + it('reverts', async () => { + await expect(increaseAllowance(tokenOwner, spender, amount)) + .to.be.revertedWith('XC20: approve to the zero address') + }) + }) + }) +}) diff --git a/packages/contracts-watr/test/ProxyWithController.test.ts b/packages/contracts-watr/test/ProxyWithController.test.ts index f29b8ff4b..2bd1bacb8 100644 --- a/packages/contracts-watr/test/ProxyWithController.test.ts +++ b/packages/contracts-watr/test/ProxyWithController.test.ts @@ -10,15 +10,18 @@ import { RegistryMock, MockTrueCurrency, OwnedUpgradeabilityProxy, - TokenControllerMock, RegistryMock__factory, MockTrueCurrency__factory, OwnedUpgradeabilityProxy__factory, - TokenControllerMock__factory, + MockXC20__factory, + MockXC20, TokenControllerV3__factory, TokenControllerV3, } from 'contracts' +import { trueUSDDecimals } from 'utils' use(solidity) +const pausedImplementationAddress = '0x3c8984DCE8f68FCDEEEafD9E0eca3598562eD291' + describe('ProxyWithController', () => { let owner: Wallet let otherWallet: Wallet @@ -31,13 +34,15 @@ describe('ProxyWithController', () => { let registry: RegistryMock + let xc20: MockXC20 + let tokenProxy: OwnedUpgradeabilityProxy let tusdImplementation: MockTrueCurrency let token: MockTrueCurrency let controllerProxy: OwnedUpgradeabilityProxy - let controllerImplementation: TokenControllerMock - let controller: TokenControllerMock + let controllerImplementation: TokenControllerV3 + let controller: TokenControllerV3 const notes = formatBytes32String('some notes') const CAN_BURN = formatBytes32String('canBurn') @@ -46,25 +51,27 @@ describe('ProxyWithController', () => { [owner, otherWallet, thirdWallet, mintKey, pauseKey, approver1, approver2, approver3] = wallets registry = await new RegistryMock__factory(owner).deploy() + xc20 = await new MockXC20__factory(owner).deploy(trueUSDDecimals) + tokenProxy = await new OwnedUpgradeabilityProxy__factory(owner).deploy() tusdImplementation = await new MockTrueCurrency__factory(owner).deploy() await tokenProxy.upgradeTo(tusdImplementation.address) token = new MockTrueCurrency__factory(owner).attach(tokenProxy.address) - await token.initialize() + await token.initialize(xc20.address) controllerProxy = await new OwnedUpgradeabilityProxy__factory(owner).deploy() - controllerImplementation = await new TokenControllerMock__factory(owner).deploy() + controllerImplementation = await new TokenControllerV3__factory(owner).deploy() await controllerProxy.upgradeTo(controllerImplementation.address) - controller = new TokenControllerMock__factory(owner).attach(controllerProxy.address) + controller = new TokenControllerV3__factory(owner).attach(controllerProxy.address) - await controller.initialize() + await controller.initialize(pausedImplementationAddress) await controller.setToken(token.address) await controller.transferMintKey(mintKey.address) }) describe('Set up proxy', () => { it('controller cannot be reinitialized', async () => { - await expect(controller.initialize()) + await expect(controller.initialize(pausedImplementationAddress)) .to.be.reverted }) @@ -92,7 +99,7 @@ describe('ProxyWithController', () => { await token.transferOwnership(controller.address) expect(await token.pendingOwner()).to.equal(controller.address) - await controller.issueClaimOwnership(token.address) + await controller.claimTrueCurrencyOwnership() expect(await token.owner()).to.equal(controller.address) }) @@ -105,7 +112,7 @@ describe('ProxyWithController', () => { await registry.setAttribute(pauseKey.address, formatBytes32String('isTUSDMintPausers'), 1, notes) await token.mint(thirdWallet.address, parseEther('1000')) await token.transferOwnership(controller.address) - await controller.issueClaimOwnership(token.address) + await controller.claimTrueCurrencyOwnership() await controller.setMintThresholds(parseEther('100'), parseEther('1000'), parseEther('10000')) await controller.setMintLimits(parseEther('300'), parseEther('3000'), parseEther('30000')) }) diff --git a/packages/contracts-watr/test/TokenController.test.ts b/packages/contracts-watr/test/TokenController.test.ts index 853205952..bd646656f 100644 --- a/packages/contracts-watr/test/TokenController.test.ts +++ b/packages/contracts-watr/test/TokenController.test.ts @@ -7,8 +7,6 @@ import { parseEther } from '@ethersproject/units' import { beforeEachWithFixture } from 'fixtures/beforeEachWithFixture' import { - TokenControllerMock__factory, - TokenControllerMock, RegistryMock, RegistryMock__factory, OwnedUpgradeabilityProxy, @@ -17,10 +15,14 @@ import { MockTrueCurrency, ForceEther, ForceEther__factory, + MockXC20__factory, TokenControllerV3__factory, TokenControllerV3, } from 'contracts' +import { trueUSDDecimals } from 'utils' use(solidity) +const pausedImplementationAddress = '0x3c8984DCE8f68FCDEEEafD9E0eca3598562eD291' + describe('TokenController', () => { let provider: MockProvider @@ -36,7 +38,7 @@ describe('TokenController', () => { let token: MockTrueCurrency let tokenImplementation: MockTrueCurrency let tokenProxy: OwnedUpgradeabilityProxy - let controller: TokenControllerMock + let controller: TokenControllerV3 let registry: RegistryMock const notes = formatBytes32String('notes') @@ -51,20 +53,21 @@ describe('TokenController', () => { provider = _provider registry = await new RegistryMock__factory(owner).deploy() - controller = await new TokenControllerMock__factory(owner).deploy() + controller = await new TokenControllerV3__factory(owner).deploy() tokenProxy = await new OwnedUpgradeabilityProxy__factory(owner).deploy() tokenImplementation = await new MockTrueCurrency__factory(owner).deploy() await tokenProxy.upgradeTo(tokenImplementation.address) + const xc20 = await new MockXC20__factory(owner).deploy(trueUSDDecimals) token = new MockTrueCurrency__factory(owner).attach(tokenProxy.address) - await token.initialize() + await token.initialize(xc20.address) await token.transferOwnership(controller.address) - await controller.initialize() - await controller.issueClaimOwnership(token.address) + await controller.initialize(pausedImplementationAddress) await controller.setRegistry(registry.address) await controller.setToken(token.address) + await controller.claimTrueCurrencyOwnership() await controller.transferMintKey(mintKey.address) await tokenProxy.transferProxyOwnership(controller.address) await controller.claimTrueCurrencyProxyOwnership() @@ -477,14 +480,14 @@ describe('TokenController', () => { describe('initialization', function () { it('controller cannot be re-initialized', async function () { - await expect(controller.initialize()) + await expect(controller.initialize(pausedImplementationAddress)) .to.be.reverted }) }) describe('transfer child', function () { it('can transfer trueUSD ownership to another address', async function () { - await controller.transferChild(token.address, owner.address) + await controller.transferTrueCurrencyOwnership(owner.address) expect(await token.pendingOwner()).to.equal(owner.address) }) }) @@ -515,7 +518,7 @@ describe('TokenController', () => { await token.connect(thirdWallet).transfer(mintKey.address, parseEther('10')) await controller.pauseToken() expect(await tokenProxy.implementation()) - .to.equal('0x3c8984DCE8f68FCDEEEafD9E0eca3598562eD291') + .to.equal(pausedImplementationAddress) }) it('non pauser cannot pause TrueUSD ', async function () { diff --git a/packages/contracts-watr/test/TrueMintableBurnable.test.ts b/packages/contracts-watr/test/TrueMintableBurnable.test.ts new file mode 100644 index 000000000..2890b7d51 --- /dev/null +++ b/packages/contracts-watr/test/TrueMintableBurnable.test.ts @@ -0,0 +1,386 @@ +import { expect, use } from 'chai' +import { solidity } from 'ethereum-waffle' +import { BigNumber, BigNumberish, constants, Wallet } from 'ethers' +import { MockXC20__factory, TrueUSD, TrueUSD__factory } from 'contracts' +import { beforeEachWithFixture } from 'fixtures/beforeEachWithFixture' +import { AddressZero, Zero } from '@ethersproject/constants' +import { parseEther } from '@ethersproject/units' +import { parseTrueUSD, trueUSDDecimals } from 'utils' + +use(solidity) + +describe('TrueCurrency - Mint/Burn', () => { + const redemptionAddress = { address: '0x0000000000000000000000000000000000074D72' } + + let owner: Wallet + let initialHolder: Wallet + let secondAccount: Wallet + let token: TrueUSD + + const initialSupply = parseTrueUSD('1000') + + beforeEachWithFixture(async (wallets) => { + [owner, initialHolder, secondAccount] = wallets + const mockXC20 = await new MockXC20__factory(owner).deploy(trueUSDDecimals) + token = await new TrueUSD__factory(owner).deploy() + await token.initialize(mockXC20.address) + await token.connect(owner).mint(initialHolder.address, initialSupply) + + await token.setBurnBounds(0, constants.MaxUint256) + await token.setCanBurn(redemptionAddress.address, true) + }) + + function approve(tokenOwner: Wallet, spender: { address: string }, amount: BigNumberish) { + const asTokenOwner = token.connect(tokenOwner) + return asTokenOwner.approve(spender.address, amount) + } + + function transfer(sender: Wallet, recipient: { address: string }, amount: BigNumberish) { + return token.connect(sender).transfer(recipient.address, amount) + } + + function transferFrom(spender: Wallet, tokenOwner: Wallet, recipient: { address: string }, amount: BigNumberish) { + return token.connect(spender).transferFrom(tokenOwner.address, recipient.address, amount) + } + + describe('transfers to redemption addresses', () => { + const initialBalance = initialSupply + let tokenOwner: Wallet + let burner: Wallet + + beforeEach(async () => { + tokenOwner = initialHolder + burner = secondAccount + }) + + it('only owner can allow burning', async () => { + await expect(token.connect(burner).setCanBurn(burner.address, true)) + .to.be.revertedWith('only Owner') + }) + + describe('transfer', () => { + describe('when the given amount is not greater than balance of the sender', () => { + describe('for a zero amount', () => { + shouldBurn(Zero) + }) + + describe('for a non-zero amount', () => { + shouldBurn(parseEther('12')) + }) + + function shouldBurn(amount: BigNumber) { + it('burns the requested amount', async () => { + await transfer(tokenOwner, redemptionAddress, amount) + expect(await token.balanceOf(tokenOwner.address)).to.eq(initialBalance.sub(amount)) + expect(await token.totalSupply()).to.eq(initialBalance.sub(amount)) + }) + + it('emits transfer and burn events', async () => { + await expect(transfer(tokenOwner, redemptionAddress, amount)) + .to.emit(token, 'Transfer').withArgs(tokenOwner.address, redemptionAddress.address, amount).and + .to.emit(token, 'Transfer').withArgs(redemptionAddress.address, constants.AddressZero, amount).and + .to.emit(token, 'Burn').withArgs(redemptionAddress.address, amount) + }) + + it('drops digits below cents', async () => { + const balanceBefore = await token.balanceOf(tokenOwner.address) + await expect(transfer(tokenOwner, redemptionAddress, amount.add(12_000))) + .to.emit(token, 'Transfer').withArgs(tokenOwner.address, redemptionAddress.address, amount).and + .to.emit(token, 'Transfer').withArgs(redemptionAddress.address, constants.AddressZero, amount).and + .to.emit(token, 'Burn').withArgs(redemptionAddress.address, amount) + expect(await token.balanceOf(tokenOwner.address)).to.equal(balanceBefore.sub(amount)) + expect(await token.balanceOf(redemptionAddress.address)).to.equal(0) + }) + } + }) + + describe('when the given amount is greater than the balance of the sender', () => { + it('reverts', async () => { + await expect(transfer(tokenOwner, redemptionAddress, initialBalance.add(parseEther('0.01')))) + .to.be.revertedWith('XC20: amount exceeds balance') + }) + }) + + describe('when redemption address is not allowed to burn', () => { + it('reverts', async () => { + await token.setCanBurn(redemptionAddress.address, false) + await expect(transfer(tokenOwner, redemptionAddress, 1)) + .to.be.revertedWith('TrueCurrency: cannot burn from this address') + }) + }) + + it('zero address is not a redemption address', async () => { + await expect(transfer(tokenOwner, { address: AddressZero }, 1)) + .to.be.revertedWith('XC20: transfer to the zero address') + }) + }) + + describe('transferFrom', () => { + describe('on success', () => { + describe('for a zero amount', () => { + shouldBurnFrom(BigNumber.from(0)) + }) + + describe('for a non-zero amount', () => { + shouldBurnFrom(parseEther('12')) + }) + + function shouldBurnFrom(amount: BigNumber) { + const originalAllowance = amount.mul(3) + + beforeEach(async () => { + await approve(tokenOwner, burner, originalAllowance) + }) + + it('burns the requested amount', async () => { + await transferFrom(burner, tokenOwner, redemptionAddress, amount) + expect(await token.balanceOf(tokenOwner.address)).to.eq(initialBalance.sub(amount)) + expect(await token.totalSupply()).to.eq(initialBalance.sub(amount)) + }) + + it('decrements allowance', async () => { + await transferFrom(burner, tokenOwner, redemptionAddress, amount) + expect(await token.allowance(tokenOwner.address, burner.address)).to.eq(originalAllowance.sub(amount)) + }) + + it('emits transfer and burn events', async () => { + await expect(transferFrom(burner, tokenOwner, redemptionAddress, amount)) + .to.emit(token, 'Transfer').withArgs(tokenOwner.address, redemptionAddress.address, amount).and + .to.emit(token, 'Transfer').withArgs(redemptionAddress.address, constants.AddressZero, amount).and + .to.emit(token, 'Burn').withArgs(redemptionAddress.address, amount) + }) + + it('drops digits below cents', async () => { + const balanceBefore = await token.balanceOf(tokenOwner.address) + await approve(tokenOwner, burner, originalAllowance.add(12_000)) + await expect(transferFrom(burner, tokenOwner, redemptionAddress, amount.add(12_000))) + .to.emit(token, 'Transfer').withArgs(tokenOwner.address, redemptionAddress.address, amount).and + .to.emit(token, 'Transfer').withArgs(redemptionAddress.address, constants.AddressZero, amount).and + .to.emit(token, 'Burn').withArgs(redemptionAddress.address, amount) + expect(await token.balanceOf(redemptionAddress.address)).to.equal(0) + expect(await token.balanceOf(tokenOwner.address)).to.equal(balanceBefore.sub(amount)) + }) + } + }) + + describe('when the given amount is greater than the balance of the sender', () => { + const amount = initialBalance.add(parseEther('0.01')) + + it('reverts', async () => { + await approve(tokenOwner, burner, amount) + await expect(transferFrom(burner, tokenOwner, redemptionAddress, amount)) + .to.be.revertedWith('XC20: amount exceeds balance') + }) + }) + + describe('when the given amount is greater than the allowance', () => { + const allowance = BigNumber.from(12_000_000) + + it('reverts', async () => { + await approve(tokenOwner, burner, allowance) + await expect(transferFrom(burner, tokenOwner, redemptionAddress, allowance.mul(2))) + .to.be.revertedWith('XC20: amount exceeds allowance') + }) + }) + + describe('when redemption address is not allowed to burn', () => { + it('reverts', async () => { + await approve(tokenOwner, burner, 1) + await token.setCanBurn(redemptionAddress.address, false) + await expect(transferFrom(burner, tokenOwner, redemptionAddress, 1)) + .to.be.revertedWith('TrueCurrency: cannot burn from this address') + }) + }) + }) + }) + + describe('setBurnBounds', () => { + function setBurnBounds(caller: Wallet, minAmount: BigNumberish, maxAmount: BigNumberish) { + return token.connect(caller).setBurnBounds(minAmount, maxAmount) + } + + let other: Wallet + + beforeEach(() => { + other = secondAccount + }) + + describe('when the caller is the contract owner', () => { + describe('when min amount is less or equal to max amount', () => { + it('sets the new burn bounds', async () => { + await setBurnBounds(owner, 10, 100) + expect(await token.burnMin()).to.eq(10) + expect(await token.burnMax()).to.eq(100) + }) + + it('emits set event', async () => { + await expect(setBurnBounds(owner, 10, 100)) + .to.emit(token, 'SetBurnBounds').withArgs(10, 100) + }) + }) + + describe('when min amount is greater than max amount', () => { + it('reverts', async () => { + await expect(setBurnBounds(owner, 100, 0)) + .to.be.revertedWith('BurnableTokenWithBounds: min > max') + }) + }) + }) + + describe('when the caller is not the contract owner', () => { + it('reverts', async () => { + await expect(setBurnBounds(other, 0, 100)) + .to.be.revertedWith('only Owner') + }) + }) + }) + + describe('mint', () => { + function mint(caller: Wallet, to: { address: string }, amount: BigNumberish) { + return token.connect(caller).mint(to.address, amount) + } + + let other: Wallet + + beforeEach(() => { + other = secondAccount + }) + + describe('when the caller is the contract owner', () => { + describe('when the target account is not the zero address', () => { + describe('for a zero amount', () => { + shouldMint(BigNumber.from(0)) + }) + + describe('for a non-zero amount', () => { + shouldMint(BigNumber.from(100)) + }) + + function shouldMint(amount: BigNumber) { + it('mints the requested amount', async () => { + const initialSupply = await token.totalSupply() + const initialBalance = await token.balanceOf(other.address) + await mint(owner, other, amount) + + expect(await token.totalSupply()).to.eq(initialSupply.add(amount)) + expect(await token.balanceOf(other.address)).to.eq(initialBalance.add(amount)) + }) + + it('emits transfer and mint events', async () => { + await expect(mint(owner, other, amount)) + .to.emit(token, 'Transfer').withArgs(constants.AddressZero, other.address, amount).and + .to.emit(token, 'Mint').withArgs(other.address, amount) + }) + } + }) + + describe('when the target account is the zero address', () => { + it('reverts', async () => { + await expect(mint(owner, { address: constants.AddressZero }, 100)) + .to.be.revertedWith('XC20: mint to the zero address') + }) + }) + + describe('when the target account is the redemption address', () => { + it('reverts', async () => { + await expect(mint(owner, redemptionAddress, 100)) + .to.be.revertedWith('TrueCurrency: account is a redemption address') + }) + }) + }) + + describe('when the caller in not the contract owner', () => { + it('reverts', async () => { + await expect(mint(other, other, 100)) + .to.be.revertedWith('only Owner') + }) + }) + }) + + describe('blacklist', () => { + let blacklistedAccount: Wallet + + beforeEach(async () => { + blacklistedAccount = secondAccount + await token.mint(blacklistedAccount.address, parseEther('1')) + await approve(blacklistedAccount, initialHolder, parseEther('1')) + await approve(initialHolder, blacklistedAccount, parseEther('1')) + await approve(initialHolder, initialHolder, parseEther('1')) + await token.setBlacklisted(blacklistedAccount.address, true) + }) + + it('emits event when blacklist status changes', async () => { + await expect(token.setBlacklisted(blacklistedAccount.address, false)) + .to.emit(token, 'Blacklisted').withArgs(blacklistedAccount.address, false) + + await expect(token.setBlacklisted(blacklistedAccount.address, true)) + .to.emit(token, 'Blacklisted').withArgs(blacklistedAccount.address, true) + }) + + describe('transfer', () => { + it('cannot transfer from blacklisted address', async () => { + await expect(transfer(blacklistedAccount, initialHolder, 1)) + .to.be.revertedWith('TrueCurrency: sender is blacklisted') + }) + + it('cannot transfer to blacklisted address', async () => { + await expect(transfer(initialHolder, blacklistedAccount, 1)) + .to.be.revertedWith('TrueCurrency: recipient is blacklisted') + }) + }) + + describe('transferFrom', () => { + it('cannot transfer from blacklisted address', async () => { + await expect(transferFrom(initialHolder, blacklistedAccount, initialHolder, 1)) + .to.be.revertedWith('TrueCurrency: sender is blacklisted') + }) + + it('cannot transfer to blacklisted address', async () => { + await expect(transferFrom(initialHolder, initialHolder, blacklistedAccount, 1)) + .to.be.revertedWith('TrueCurrency: recipient is blacklisted') + }) + + it('blacklisted address cannot call transferFrom', async () => { + await expect(transferFrom(blacklistedAccount, initialHolder, redemptionAddress, 1)) + .to.be.revertedWith('TrueCurrency: tokens spender is blacklisted') + }) + }) + + it('cannot mint to blacklisted account', async () => { + await expect(token.mint(blacklistedAccount.address, 1)).to.be.revertedWith('TrueCurrency: account is blacklisted') + }) + + describe('approve', () => { + it('blacklisted account cannot approve', async () => { + await expect(approve(blacklistedAccount, initialHolder, 1)) + .to.be.revertedWith('TrueCurrency: tokens owner is blacklisted') + }) + + it('cannot approve to blacklisted account', async () => { + await expect(approve(initialHolder, blacklistedAccount, 1)) + .to.be.revertedWith('TrueCurrency: tokens spender is blacklisted') + }) + + it('can remove approval for blacklisted account', async () => { + await expect(approve(initialHolder, blacklistedAccount, 0)).to.be.not.reverted + }) + + it('blacklisted account cannot remove approval', async () => { + await expect(approve(blacklistedAccount, initialHolder, 0)).to.be.revertedWith('TrueCurrency: tokens owner is blacklisted') + }) + }) + + describe('when non owner tries to change blacklist', () => { + it('reverts', async () => { + await expect(token.connect(blacklistedAccount).setBlacklisted(blacklistedAccount.address, false)) + .to.be.revertedWith('only Owner') + }) + }) + + describe('when blacklisting redemption address', () => { + it('reverts', async () => { + await expect(token.setBlacklisted(AddressZero, true)).to.be.revertedWith('TrueCurrency: blacklisting of redemption address is not allowed') + }) + }) + }) +}) diff --git a/packages/contracts-watr/test/TrueUSD.test.ts b/packages/contracts-watr/test/TrueUSD.test.ts index 1163c8464..8a3957e81 100644 --- a/packages/contracts-watr/test/TrueUSD.test.ts +++ b/packages/contracts-watr/test/TrueUSD.test.ts @@ -4,13 +4,18 @@ import { expect, use } from 'chai' import { beforeEachWithFixture } from 'fixtures/beforeEachWithFixture' import { waffle } from 'hardhat' -import { timeTravel } from 'utils/timeTravel' +import { timeTravel, trueUSDDecimals } from 'utils' import { + MockTrueCurrency__factory, MockV3Aggregator, MockV3Aggregator__factory, + MockXC20, + MockXC20__factory, + OwnedUpgradeabilityProxy, + OwnedUpgradeabilityProxy__factory, TrueUSD, - TrueUSD__factory, } from 'contracts' +import { MockProvider } from 'ethereum-waffle' use(waffle.solidity) @@ -23,18 +28,27 @@ describe('TrueCurrency with Proof-of-reserves check', () => { const ONE_DAY_SECONDS = 24 * 60 * 60 // seconds in a day const TUSD_FEED_INITIAL_ANSWER = exp(1_000_000, 18).toString() // '1M TUSD in reserves' const AMOUNT_TO_MINT = utils.parseEther('1000000') + let tusdImplementation: TrueUSD let token: TrueUSD + let tokenProxy: OwnedUpgradeabilityProxy let mockV3Aggregator: MockV3Aggregator let owner: Wallet - let provider: providers.JsonRpcProvider + let provider: MockProvider + let xc20: MockXC20 beforeEachWithFixture(async (wallets, _provider) => { [owner] = wallets provider = _provider - token = (await new TrueUSD__factory(owner).deploy()) as TrueUSD + tokenProxy = await new OwnedUpgradeabilityProxy__factory(owner).deploy() + tusdImplementation = await new MockTrueCurrency__factory(owner).deploy() + await tokenProxy.upgradeTo(tusdImplementation.address) + xc20 = await new MockXC20__factory(owner).deploy(trueUSDDecimals) + token = new MockTrueCurrency__factory(owner).attach(tokenProxy.address) + await token.initialize(xc20.address) + await token.mint(owner.address, AMOUNT_TO_MINT) // Deploy a mock aggregator to mock Proof of Reserve feed answers mockV3Aggregator = await new MockV3Aggregator__factory(owner).deploy( - '18', + trueUSDDecimals, TUSD_FEED_INITIAL_ANSWER, ) // Reset pool Proof Of Reserve feed defaults diff --git a/packages/contracts-watr/test/utils/index.ts b/packages/contracts-watr/test/utils/index.ts new file mode 100644 index 000000000..cf03b5bf6 --- /dev/null +++ b/packages/contracts-watr/test/utils/index.ts @@ -0,0 +1,2 @@ +export * from './timeTravel' +export * from './parseTrueUSD' diff --git a/packages/contracts-watr/test/utils/parseTrueUSD.ts b/packages/contracts-watr/test/utils/parseTrueUSD.ts new file mode 100644 index 000000000..c8437d9c0 --- /dev/null +++ b/packages/contracts-watr/test/utils/parseTrueUSD.ts @@ -0,0 +1,6 @@ +import { parseUnits } from 'ethers/lib/utils' + +export const trueUSDDecimals = 18 +export function parseTrueUSD(amount: string) { + return parseUnits(amount, trueUSDDecimals) +} diff --git a/packages/contracts-watr/test/verifyDeployment.test.ts b/packages/contracts-watr/test/verifyDeployment.test.ts deleted file mode 100644 index dec052fb4..000000000 --- a/packages/contracts-watr/test/verifyDeployment.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Contract } from 'ethers' -import { JsonRpcProvider } from '@ethersproject/providers' -import { expect } from 'chai' -import { unknown as deployments } from '../deployments-watr_local.json' - -describe('verify deployment', () => { - const provider = new JsonRpcProvider('http://127.0.0.1:8822', 688) - const ownableInterface = [ - 'function owner() view returns (address)', - 'function proxyOwner() view returns (address)', - ] - const controllerInterface = [ - ...ownableInterface, - 'function token() view returns (address)', - 'function registry() view returns (address)', - ] - - const ownableContract = (address: string) => new Contract( - address, - controllerInterface, - provider, - ) - - const controllerContract = (address: string) => new Contract( - address, - controllerInterface, - provider, - ) - - it('controller owns currency', async () => { - const contract = ownableContract(deployments.trueUSD_proxy.address) - - const owner = await contract.owner() - const proxyOwner = await contract.proxyOwner() - - expect(owner).to.eq(deployments.tokenControllerV3_proxy.address) - expect(proxyOwner).to.eq(deployments.tokenControllerV3_proxy.address) - }) - - it('controller has currency set as token', async () => { - const contract = controllerContract(deployments.tokenControllerV3_proxy.address) - - const token = await contract.token() - - expect(token).to.eq(deployments.trueUSD_proxy.address) - }) - - it('controller has registry set correctly', async () => { - const contract = controllerContract(deployments.tokenControllerV3_proxy.address) - - const token = await contract.registry() - - expect(token).to.eq(deployments.registry_proxy.address) - }) -}) diff --git a/packages/contracts-watr/test/verifyDeployment/verifyDeployment.test.ts b/packages/contracts-watr/test/verifyDeployment/verifyDeployment.test.ts new file mode 100644 index 000000000..ad3397ef0 --- /dev/null +++ b/packages/contracts-watr/test/verifyDeployment/verifyDeployment.test.ts @@ -0,0 +1,82 @@ +import { Contract, ethers } from 'ethers' +import { JsonRpcProvider } from '@ethersproject/providers' +import { expect, use } from 'chai' +import { unknown as deployments } from '../../deployments-watr_local.json' +import { TokenControllerV3__factory, TrueUSD__factory } from 'contracts' +import { parseEther } from '@ethersproject/units' +import { solidity } from 'ethereum-waffle' + +use(solidity) + +describe('verify deployment', () => { + const localWatrUrl = 'http://127.0.0.1:8822' + const chainId = 688 + const provider = new JsonRpcProvider(localWatrUrl, chainId) + const ownableInterface = [ + 'function owner() view returns (address)', + 'function proxyOwner() view returns (address)', + ] + const ownableContract = (address: string) => new Contract( + address, + ownableInterface, + provider, + ) + + it('controller owns currency', async () => { + const contract = ownableContract(deployments.trueUSD_proxy.address) + + const owner = await contract.owner() + const proxyOwner = await contract.proxyOwner() + + expect(owner).to.eq(deployments.tokenControllerV3_proxy.address) + expect(proxyOwner).to.eq(deployments.tokenControllerV3_proxy.address) + }) + + it('controller has currency set as token', async () => { + const contract = TokenControllerV3__factory.connect(deployments.tokenControllerV3_proxy.address, provider) + + const token = await contract.token() + + expect(token).to.eq(deployments.trueUSD_proxy.address) + }) + + it('controller has registry set correctly', async () => { + const contract = TokenControllerV3__factory.connect(deployments.tokenControllerV3_proxy.address, provider) + + const token = await contract.registry() + + expect(token).to.eq(deployments.registry_proxy.address) + }) + + it('can mint', async () => { + const deployer = new ethers.Wallet(process.env['PRIVATE_KEY_DEPLOYER'], provider) + const tokenController = TokenControllerV3__factory.connect(deployments.tokenControllerV3_proxy.address, deployer) + const token = TrueUSD__factory.connect(deployments.trueUSD_proxy.address, deployer) + const accountAddress = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984' + + const tx = () => waitFor(tokenController.instantMint(accountAddress, parseEther('1'))) + + await expect(tx).to.changeTokenBalance(token, toAccount(accountAddress), parseEther('1')) + }) + + it('can transfer', async () => { + const deployer = new ethers.Wallet(process.env['PRIVATE_KEY_DEPLOYER'], provider) + const tokenController = TokenControllerV3__factory.connect(deployments.tokenControllerV3_proxy.address, deployer) + const token = TrueUSD__factory.connect(deployments.trueUSD_proxy.address, deployer) + const otherAddress = '0x50D1c9771902476076eCFc8B2A83Ad6b9355a4c9' + + await waitFor(tokenController.instantMint(deployer.address, parseEther('1'))) + + const tx = () => waitFor(token.transfer(otherAddress, parseEther('0.5'))) + + await expect(tx).to.changeTokenBalances(token, [deployer, toAccount(otherAddress)], [parseEther('-0.5'), parseEther('0.5')]) + }).timeout(100000) +}) + +async function waitFor(tx: Promise<{ wait: () => Promise }>) { + return (await tx).wait() +} + +function toAccount(address: string) { + return { getAddress: () => address } +} diff --git a/packages/contracts-watr/utils/bash/marsDeploy.sh b/packages/contracts-watr/utils/bash/marsDeploy.sh index 148c09f97..7bb015d5b 100755 --- a/packages/contracts-watr/utils/bash/marsDeploy.sh +++ b/packages/contracts-watr/utils/bash/marsDeploy.sh @@ -87,7 +87,7 @@ fi timestamp_log="-$(date +%s)" yarn mars -ts-node ${DEPLOY_SCRIPT} \ +dotenv -e .env.deploy -- ts-node ${DEPLOY_SCRIPT} \ --waffle-config ./.waffle.json \ --network "$network" \ --out-file "deployments-${network_name}.json" \ diff --git a/yarn.lock b/yarn.lock index d3af005d4..ad4bc1563 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4448,6 +4448,15 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -4830,6 +4839,26 @@ domutils@^2.4.3, domutils@^2.4.4: domelementtype "^2.0.1" domhandler "^4.0.0" +dotenv-cli@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.0.0.tgz#b24e842a4c00928ec91d051ab152cd927c66ad97" + integrity sha512-XfMzVdpdDTRnlcgvFLg3lSyiLXqFxS4tH7RbK5IxkC4XIUuxPyrGoDufkfLjy/dA28EILzEu+mros6h8aQmyGg== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.0.0" + dotenv-expand "^10.0.0" + minimist "^1.2.6" + +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@^16.0.0, dotenv@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" @@ -9587,6 +9616,11 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -10591,11 +10625,23 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + shelljs@^0.8.3: version "0.8.4" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" @@ -13218,7 +13264,7 @@ which@1.3.1, which@^1.1.1, which@^1.2.9, which@^1.3.1: dependencies: isexe "^2.0.0" -which@2.0.2: +which@2.0.2, which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==