diff --git a/script/printBytes3Ticker.s.sol b/script/printBytes3Ticker.s.sol new file mode 100644 index 0000000..9d91f0c --- /dev/null +++ b/script/printBytes3Ticker.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; + +contract PrintBytes3TickerScript is Script { + function run() view external { + console.log("EUR"); + console.logBytes3(bytes3("EUR")); + console.log("GBP"); + console.logBytes3(bytes3("GBP")); + console.log("USD"); + console.logBytes3(bytes3("USD")); + console.log("ISK"); + console.logBytes3(bytes3("ISK")); + } +} diff --git a/src/ControllerToken.sol b/src/ControllerToken.sol index c3f80ef..b9fd931 100644 --- a/src/ControllerToken.sol +++ b/src/ControllerToken.sol @@ -5,6 +5,9 @@ pragma solidity 0.8.20; import "./Token.sol"; import "./interfaces/IERC677Recipient.sol"; +// This contract is used for local testing. +// In production please used the network specific ControllerToken contract. i.e. EthereumControllerToken, PolygonControllerToken, etc. + // The ControllerToken contract acts as a bridge to ensure compatibility between the Smart-Contract v2 and the v1 TokenFrontend. // It allows the v2's proxy to function as the controller for the v1 TokenFrontend. // The ambition is to allow the v2's proxy to be the only contract that needs to be upgraded in the future. diff --git a/src/EthereumControllerToken.sol b/src/EthereumControllerToken.sol new file mode 100644 index 0000000..c1bc661 --- /dev/null +++ b/src/EthereumControllerToken.sol @@ -0,0 +1,172 @@ +pragma solidity 0.8.20; + +//SPDX-License-Identifier: APACHE-2.0 + +import "./Token.sol"; +import "./interfaces/IERC677Recipient.sol"; + +// The EthereumControllerToken contract acts as a bridge to ensure compatibility between the Smart-Contract v2 and the v1 TokenFrontend. +// It allows the v2's proxy to function as the controller for the v1 TokenFrontend. +// The ambition is to allow the v2's proxy to be the only contract that needs to be upgraded in the future. +contract EthereumControllerToken is Token { + struct ControllerTokenStorage { + bytes3 ticker; + } + + bytes32 private constant ControllerStorageLocation = + 0xca4de6ad8795ef60887d43e50c5ce757428c24696bc0badb9e89cdef76bfe2c9; + + function _getControllerStorage() + internal + pure + returns (ControllerTokenStorage storage $) + { + assembly { + $.slot := ControllerStorageLocation + } + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + string memory name, + string memory symbol, + bytes3 _ticker, + address _validator + ) public initializer { + ControllerTokenStorage storage $ = _getControllerStorage(); + super.initialize(name, symbol, _validator); + $.ticker = _ticker; + } + + modifier onlyFrontend() { + require( + isFrontend(msg.sender), + "ControllerToken: caller is not the frontend" + ); + _; + } + + function isFrontend(address _address) public view returns (bool) { + return (_address == getFrontend()); + } + + function getFrontend() public view returns (address) { + bytes3 t = ticker(); + if (t == 0x455552) { + // EUR + return 0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f; + } else if (t == 0x474250) { + // GBP + return 0x7ba92741Bf2A568abC6f1D3413c58c6e0244F8fD; + } else if (t == 0x555344) { + // USD + return 0xBc5142e0CC5eB16b47c63B0f033d4c2480853a52; + } else if (t == 0x49534b) { + // ISK + return 0xC642549743A93674cf38D6431f75d6443F88E3E2; + } else { + revert("Unsupported ticker"); + } + } + + function ticker() public view returns (bytes3) { + return _getControllerStorage().ticker; + } + + function transfer_withCaller( + address caller, + address to, + uint256 amount + ) external onlyFrontend returns (bool) { + require( + validator.validate(caller, to, amount), + "Transfer not validated" + ); + _transfer(caller, to, amount); + return true; + } + + function transferFrom_withCaller( + address caller, + address from, + address to, + uint256 amount + ) external onlyFrontend returns (bool) { + require(validator.validate(from, to, amount), "Transfer not validated"); + _spendAllowance(from, caller, amount); + _transfer(from, to, amount); + return true; + } + + function approve_withCaller( + address caller, + address spender, + uint256 amount + ) external onlyFrontend returns (bool) { + _approve(caller, spender, amount); + return true; + } + + function transferAndCall_withCaller( + address caller, + address to, + uint256 amount, + bytes calldata data + ) external onlyFrontend returns (bool) { + require( + validator.validate(caller, to, amount), + "Transfer not validated" + ); + _transfer(caller, to, amount); + IERC677Recipient recipient = IERC677Recipient(to); + require( + recipient.onTokenTransfer(caller, amount, data), + "token handler returns false" + ); + return true; + } + + function mintTo_withCaller( + address caller, + address to, + uint256 amount + ) external onlyFrontend onlySystemAccount(caller) returns (bool) { + _useMintAllowance(caller, amount); + _mint(to, amount); + + return true; + } + + function burnFrom_withCaller( + address caller, + address, //from + uint256, //amount + bytes32, //h + uint8, //v + bytes32, //r + bytes32 //s + ) external view onlyFrontend onlySystemAccount(caller) returns (bool) { + revert("deprecated"); + } + + function recover_withCaller( + address caller, + address, //from + address, //to + bytes32, //h + uint8, //v + bytes32, //r + bytes32 //s + ) external view onlyFrontend onlySystemAccount(caller) returns (uint256) { + revert("deprecated"); + } + + function claimOwnership() external onlyFrontend { + acceptOwnership(); + } +} + diff --git a/src/GnosisControllerToken.sol b/src/GnosisControllerToken.sol new file mode 100644 index 0000000..8252f08 --- /dev/null +++ b/src/GnosisControllerToken.sol @@ -0,0 +1,172 @@ +pragma solidity 0.8.20; + +//SPDX-License-Identifier: APACHE-2.0 + +import "./Token.sol"; +import "./interfaces/IERC677Recipient.sol"; + +// The GnosisControllerToken contract acts as a bridge to ensure compatibility between the Smart-Contract v2 and the v1 TokenFrontend. +// It allows the v2's proxy to function as the controller for the v1 TokenFrontend. +// The ambition is to allow the v2's proxy to be the only contract that needs to be upgraded in the future. +contract GnosisControllerToken is Token { + struct ControllerTokenStorage { + bytes3 ticker; + } + + bytes32 private constant ControllerStorageLocation = + 0xca4de6ad8795ef60887d43e50c5ce757428c24696bc0badb9e89cdef76bfe2c9; + + function _getControllerStorage() + internal + pure + returns (ControllerTokenStorage storage $) + { + assembly { + $.slot := ControllerStorageLocation + } + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + string memory name, + string memory symbol, + bytes3 _ticker, + address _validator + ) public initializer { + ControllerTokenStorage storage $ = _getControllerStorage(); + super.initialize(name, symbol, _validator); + $.ticker = _ticker; + } + + modifier onlyFrontend() { + require( + isFrontend(msg.sender), + "ControllerToken: caller is not the frontend" + ); + _; + } + + function isFrontend(address _address) public view returns (bool) { + return (_address == getFrontend()); + } + + function getFrontend() public view returns (address) { + bytes3 t = ticker(); + if (t == 0x455552) { + // EUR + return 0xcB444e90D8198415266c6a2724b7900fb12FC56E; + } else if (t == 0x474250) { + // GBP + return 0x5Cb9073902F2035222B9749F8fB0c9BFe5527108; + } else if (t == 0x555344) { + // USD + return 0x20E694659536C6B46e4B8BE8f6303fFCD8d1dF69; + } else if (t == 0x49534b) { + // ISK + return 0xD8F84BF2E036A3c8E4c0e25ed2aAe0370F3CCca8; + } else { + revert("Unsupported ticker"); + } + } + + function ticker() public view returns (bytes3) { + return _getControllerStorage().ticker; + } + + function transfer_withCaller( + address caller, + address to, + uint256 amount + ) external onlyFrontend returns (bool) { + require( + validator.validate(caller, to, amount), + "Transfer not validated" + ); + _transfer(caller, to, amount); + return true; + } + + function transferFrom_withCaller( + address caller, + address from, + address to, + uint256 amount + ) external onlyFrontend returns (bool) { + require(validator.validate(from, to, amount), "Transfer not validated"); + _spendAllowance(from, caller, amount); + _transfer(from, to, amount); + return true; + } + + function approve_withCaller( + address caller, + address spender, + uint256 amount + ) external onlyFrontend returns (bool) { + _approve(caller, spender, amount); + return true; + } + + function transferAndCall_withCaller( + address caller, + address to, + uint256 amount, + bytes calldata data + ) external onlyFrontend returns (bool) { + require( + validator.validate(caller, to, amount), + "Transfer not validated" + ); + _transfer(caller, to, amount); + IERC677Recipient recipient = IERC677Recipient(to); + require( + recipient.onTokenTransfer(caller, amount, data), + "token handler returns false" + ); + return true; + } + + function mintTo_withCaller( + address caller, + address to, + uint256 amount + ) external onlyFrontend onlySystemAccount(caller) returns (bool) { + _useMintAllowance(caller, amount); + _mint(to, amount); + + return true; + } + + function burnFrom_withCaller( + address caller, + address, //from + uint256, //amount + bytes32, //h + uint8, //v + bytes32, //r + bytes32 //s + ) external view onlyFrontend onlySystemAccount(caller) returns (bool) { + revert("deprecated"); + } + + function recover_withCaller( + address caller, + address, //from + address, //to + bytes32, //h + uint8, //v + bytes32, //r + bytes32 //s + ) external view onlyFrontend onlySystemAccount(caller) returns (uint256) { + revert("deprecated"); + } + + function claimOwnership() external onlyFrontend { + acceptOwnership(); + } +} + diff --git a/src/PolygonControllerToken.sol b/src/PolygonControllerToken.sol new file mode 100644 index 0000000..9aac33c --- /dev/null +++ b/src/PolygonControllerToken.sol @@ -0,0 +1,172 @@ +pragma solidity 0.8.20; + +//SPDX-License-Identifier: APACHE-2.0 + +import "./Token.sol"; +import "./interfaces/IERC677Recipient.sol"; + +// The PolygonControllerToken contract acts as a bridge to ensure compatibility between the Smart-Contract v2 and the v1 TokenFrontend. +// It allows the v2's proxy to function as the controller for the v1 TokenFrontend. +// The ambition is to allow the v2's proxy to be the only contract that needs to be upgraded in the future. +contract PolygonControllerToken is Token { + struct ControllerTokenStorage { + bytes3 ticker; + } + + bytes32 private constant ControllerStorageLocation = + 0xca4de6ad8795ef60887d43e50c5ce757428c24696bc0badb9e89cdef76bfe2c9; + + function _getControllerStorage() + internal + pure + returns (ControllerTokenStorage storage $) + { + assembly { + $.slot := ControllerStorageLocation + } + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + string memory name, + string memory symbol, + bytes3 _ticker, + address _validator + ) public initializer { + ControllerTokenStorage storage $ = _getControllerStorage(); + super.initialize(name, symbol, _validator); + $.ticker = _ticker; + } + + modifier onlyFrontend() { + require( + isFrontend(msg.sender), + "ControllerToken: caller is not the frontend" + ); + _; + } + + function isFrontend(address _address) public view returns (bool) { + return (_address == getFrontend()); + } + + function getFrontend() public view returns (address) { + bytes3 t = ticker(); + if (t == 0x455552) { + // EUR + return 0x18ec0A6E18E5bc3784fDd3a3634b31245ab704F6; + } else if (t == 0x474250) { + // GBP + return 0x75792CBDb361d80ba89271a079EfeE62c29FA324; + } else if (t == 0x555344) { + // USD + return 0x64E97c1a6535afD4a313eF46F88A64a34250B719; + } else if (t == 0x49534b) { + // ISK + return 0xf1bBf27A9D659D326efBfa5D284EBaeFB803983D; + } else { + revert("Unsupported ticker"); + } + } + + function ticker() public view returns (bytes3) { + return _getControllerStorage().ticker; + } + + function transfer_withCaller( + address caller, + address to, + uint256 amount + ) external onlyFrontend returns (bool) { + require( + validator.validate(caller, to, amount), + "Transfer not validated" + ); + _transfer(caller, to, amount); + return true; + } + + function transferFrom_withCaller( + address caller, + address from, + address to, + uint256 amount + ) external onlyFrontend returns (bool) { + require(validator.validate(from, to, amount), "Transfer not validated"); + _spendAllowance(from, caller, amount); + _transfer(from, to, amount); + return true; + } + + function approve_withCaller( + address caller, + address spender, + uint256 amount + ) external onlyFrontend returns (bool) { + _approve(caller, spender, amount); + return true; + } + + function transferAndCall_withCaller( + address caller, + address to, + uint256 amount, + bytes calldata data + ) external onlyFrontend returns (bool) { + require( + validator.validate(caller, to, amount), + "Transfer not validated" + ); + _transfer(caller, to, amount); + IERC677Recipient recipient = IERC677Recipient(to); + require( + recipient.onTokenTransfer(caller, amount, data), + "token handler returns false" + ); + return true; + } + + function mintTo_withCaller( + address caller, + address to, + uint256 amount + ) external onlyFrontend onlySystemAccount(caller) returns (bool) { + _useMintAllowance(caller, amount); + _mint(to, amount); + + return true; + } + + function burnFrom_withCaller( + address caller, + address, //from + uint256, //amount + bytes32, //h + uint8, //v + bytes32, //r + bytes32 //s + ) external view onlyFrontend onlySystemAccount(caller) returns (bool) { + revert("deprecated"); + } + + function recover_withCaller( + address caller, + address, //from + address, //to + bytes32, //h + uint8, //v + bytes32, //r + bytes32 //s + ) external view onlyFrontend onlySystemAccount(caller) returns (uint256) { + revert("deprecated"); + } + + function claimOwnership() external onlyFrontend { + acceptOwnership(); + } +} + diff --git a/test/ControllerToken.t.sol b/test/ControllerToken.t.sol index a086dc7..507e7f9 100644 --- a/test/ControllerToken.t.sol +++ b/test/ControllerToken.t.sol @@ -8,6 +8,10 @@ import "../src/tests/tokenfrontend.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "forge-std/console.sol"; +import "../src/EthereumControllerToken.sol"; +import "../src/PolygonControllerToken.sol"; +import "../src/GnosisControllerToken.sol"; + //import "forge-std/Vm.sol"; contract ControllerTokenTest is Test { @@ -88,6 +92,93 @@ contract ControllerTokenTest is Test { assertEq(token.ticker(), bytes3("EUR")); } + function test_ethereum_eur_should_return_right_frontend() public { + bytes memory initData = abi.encodeWithSelector( + EthereumControllerToken.initialize.selector, + "Monerium EUR emoney", + "EURE", + bytes3("EUR"), + address(validator) + ); + + EthereumControllerToken implementation = new EthereumControllerToken(); + EthereumControllerToken p = EthereumControllerToken( + address(new ERC1967Proxy(address(implementation), initData)) + ); + EthereumControllerToken ethereumToken = EthereumControllerToken( + address(p) + ); + assertEq( + ethereumToken.getFrontend(), + 0x3231Cb76718CDeF2155FC47b5286d82e6eDA273f + ); + } + function test_ethereum_usd_should_return_right_frontend() public { + bytes memory initData = abi.encodeWithSelector( + EthereumControllerToken.initialize.selector, + "Monerium USD emoney", + "USDE", + bytes3("USD"), + address(validator) + ); + + EthereumControllerToken implementation = new EthereumControllerToken(); + EthereumControllerToken p = EthereumControllerToken( + address(new ERC1967Proxy(address(implementation), initData)) + ); + EthereumControllerToken ethereumToken = EthereumControllerToken( + address(p) + ); + assertEq( + ethereumToken.getFrontend(), + 0xBc5142e0CC5eB16b47c63B0f033d4c2480853a52 + ); + } + + function test_ethereum_gbp_should_return_right_frontend() public { + bytes memory initData = abi.encodeWithSelector( + EthereumControllerToken.initialize.selector, + "Monerium GBP emoney", + "GBPE", + bytes3("GBP"), + address(validator) + ); + + EthereumControllerToken implementation = new EthereumControllerToken(); + EthereumControllerToken p = EthereumControllerToken( + address(new ERC1967Proxy(address(implementation), initData)) + ); + EthereumControllerToken ethereumToken = EthereumControllerToken( + address(p) + ); + assertEq( + ethereumToken.getFrontend(), + 0x7ba92741Bf2A568abC6f1D3413c58c6e0244F8fD + ); + } + + function test_ethereum_isk_should_return_right_frontend() public { + bytes memory initData = abi.encodeWithSelector( + EthereumControllerToken.initialize.selector, + "Monerium ISK emoney", + "ISKE", + bytes3("ISK"), + address(validator) + ); + + EthereumControllerToken implementation = new EthereumControllerToken(); + EthereumControllerToken p = EthereumControllerToken( + address(new ERC1967Proxy(address(implementation), initData)) + ); + EthereumControllerToken ethereumToken = EthereumControllerToken( + address(p) + ); + assertEq( + ethereumToken.getFrontend(), + 0xC642549743A93674cf38D6431f75d6443F88E3E2 + ); + } + function test_shouldTransfer() public { vm.prank(user1); frontend.transfer(user2, 1e18);