From cec3161488400238e06f9b2dfab0ce98435e5dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BBuk?= Date: Fri, 18 Aug 2023 12:46:40 +0200 Subject: [PATCH] Upgradeable ERC-20 Tokens --- .github/workflows/test-solidity.yml | 4 +- eth-bridge/contracts/README.md | 42 +++++--- eth-bridge/contracts/lib/forge-std | 2 +- .../contracts/lib/openzeppelin-contracts | 2 +- .../lib/openzeppelin-contracts-upgradeable | 2 +- eth-bridge/contracts/script/Deploy.s.sol | 26 ++++- .../{Upgrade.s.sol => UpgradeBridges.s.sol} | 6 +- .../contracts/script/UpgradeTokens.s.sol | 20 ++++ eth-bridge/contracts/src/Bridge.sol | 71 +------------ eth-bridge/contracts/src/BridgeEvents.sol | 33 ++++++ eth-bridge/contracts/src/BridgeTypes.sol | 27 +++++ eth-bridge/contracts/src/WrappedToken.sol | 54 +++++++--- eth-bridge/contracts/test/Bridge.t.sol | 71 ++++++------- .../contracts/test/VoteFeeCompare.t.sol | 50 +++++---- eth-bridge/contracts/test/WrappedToken.t.sol | 100 +++++++++++++++++- 15 files changed, 340 insertions(+), 170 deletions(-) rename eth-bridge/contracts/script/{Upgrade.s.sol => UpgradeBridges.s.sol} (66%) create mode 100644 eth-bridge/contracts/script/UpgradeTokens.s.sol create mode 100644 eth-bridge/contracts/src/BridgeEvents.sol create mode 100644 eth-bridge/contracts/src/BridgeTypes.sol diff --git a/.github/workflows/test-solidity.yml b/.github/workflows/test-solidity.yml index d0c099686..315d5f782 100644 --- a/.github/workflows/test-solidity.yml +++ b/.github/workflows/test-solidity.yml @@ -53,7 +53,7 @@ jobs: working-directory: ./eth-bridge/contracts/ run: | forge --version - forge build --sizes + forge build --build-info --sizes id: build_solidity - name: Run Forge tests @@ -73,4 +73,4 @@ jobs: with: target: ./eth-bridge/contracts/ slither-config: ./eth-bridge/contracts/slither.config.json - ignore-compilation: true + ignore-compile: true diff --git a/eth-bridge/contracts/README.md b/eth-bridge/contracts/README.md index 0491b7f55..12d60d648 100644 --- a/eth-bridge/contracts/README.md +++ b/eth-bridge/contracts/README.md @@ -163,34 +163,44 @@ Get contract addresses (replace path): > jq '.transactions | map(select(.transactionType == "CREATE")) | map(.contractName, .contractAddress)' broadcast/Deploy.s.sol/31337/run-latest.json [ "Bridge", - "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35", "WrappedToken", - "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "0xA15BB66138824a1c7167f5E85b957d04Dd34E468", "ERC1967Proxy", - "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - "WrappedToken", - "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "0xb19b36b1456E65E3A6D514D3F715f204BD59f431", + "ERC1967Proxy", + "0x8ce361602B935680E8DeC218b820ff5056BeB7af", "ERC1967Proxy", - "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + "0x0C8E79F3534B00D9a3D4a856B665Bf4eBC22f2ba", + "ERC1967Proxy", + "0xeD1DB453C3156Ff3155a97AD217b3087D5Dc5f6E" ] ``` -Note that `Bridge` is underlying implementation and you should interact with Proxies. +Note that `Bridge` and `WrappedToken` are underlying implementation and you can't use it directly - you must interact with Proxies. -Take the WrappedToken addresses and check which is which: +Take addresses of proxies and check which are tokens: ``` -> cast call 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 'name()' | cast --to-ascii - Liberland Dollars -> cast call 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 'name()' | cast --to-ascii +> cast call 0xeD1DB453C3156Ff3155a97AD217b3087D5Dc5f6E 'name()' | cast --to-ascii +Error: +(code: 3, message: execution reverted, data: Some(String("0x"))) + +> cast call 0x0C8E79F3534B00D9a3D4a856B665Bf4eBC22f2ba 'name()' | cast --to-ascii Liberland Merits +> cast call 0x8ce361602B935680E8DeC218b820ff5056BeB7af 'name()' | cast --to-ascii +Error: +(code: 3, message: execution reverted, data: Some(String("0x"))) + +> cast call 0xb19b36b1456E65E3A6D514D3F715f204BD59f431 'name()' | cast --to-ascii + Liberland Dollars ``` -Take the Proxy addresses and check which tokens they use: +Take the proxy addresses that failed - these are bridge proxies - and check which tokens they use: ``` -> cast call 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 'token()' -0x000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f0512 # LLD Token address +> cast call 0x8ce361602B935680E8DeC218b820ff5056BeB7af 'token()' +0x000000000000000000000000b19b36b1456e65e3a6d514d3f715f204bd59f431 # LLD Token address -> cast call 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 'token()' -0x000000000000000000000000Dc64a140Aa3E981100a9becA4E685f962f0cF6C9 # LLM Token address +> cast call 0xeD1DB453C3156Ff3155a97AD217b3087D5Dc5f6E 'token()' +0x0000000000000000000000000c8e79f3534b00d9a3d4a856b665bf4ebc22f2ba # LLM Token address ``` License: MIT diff --git a/eth-bridge/contracts/lib/forge-std b/eth-bridge/contracts/lib/forge-std index fc560fa34..7b4876e8d 160000 --- a/eth-bridge/contracts/lib/forge-std +++ b/eth-bridge/contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit fc560fa34fa12a335a50c35d92e55a6628ca467c +Subproject commit 7b4876e8de2a232a54159035f173e35421000c19 diff --git a/eth-bridge/contracts/lib/openzeppelin-contracts b/eth-bridge/contracts/lib/openzeppelin-contracts index 0a25c1940..fd81a96f0 160000 --- a/eth-bridge/contracts/lib/openzeppelin-contracts +++ b/eth-bridge/contracts/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 0a25c1940ca220686588c4af3ec526f725fe2582 +Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90 diff --git a/eth-bridge/contracts/lib/openzeppelin-contracts-upgradeable b/eth-bridge/contracts/lib/openzeppelin-contracts-upgradeable index 58fa0f81c..3d4c0d574 160000 --- a/eth-bridge/contracts/lib/openzeppelin-contracts-upgradeable +++ b/eth-bridge/contracts/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21 +Subproject commit 3d4c0d5741b131c231e558d7a6213392ab3672a5 diff --git a/eth-bridge/contracts/script/Deploy.s.sol b/eth-bridge/contracts/script/Deploy.s.sol index 5a08df3b0..a3e86f49d 100644 --- a/eth-bridge/contracts/script/Deploy.s.sol +++ b/eth-bridge/contracts/script/Deploy.s.sol @@ -19,8 +19,16 @@ contract Deploy is Script { uint32 votesRequired = 5; Bridge bridgeImpl = new Bridge(); + WrappedToken tokenImpl = new WrappedToken(); - WrappedToken lld = new WrappedToken("Liberland Dollars", "LLD"); + WrappedToken lld = WrappedToken( + address( + new ERC1967Proxy( + address(tokenImpl), + abi.encodeCall(WrappedToken.initialize, ("Liberland Dollars", "LLD")) + ) + ) + ); ERC1967Proxy lldBridge = new ERC1967Proxy( address(bridgeImpl), abi.encodeCall( @@ -37,9 +45,17 @@ contract Deploy is Script { ) ) ); - lld.transferOwnership(address(lldBridge)); + lld.grantRole(lld.MINTER_ROLE(), address(lldBridge)); + lld.grantRole(lld.PAUSER_ROLE(), address(lldBridge)); - WrappedToken llm = new WrappedToken("Liberland Merits", "LLM"); + WrappedToken llm = WrappedToken( + address( + new ERC1967Proxy( + address(tokenImpl), + abi.encodeCall(WrappedToken.initialize, ("Liberland Merits", "LLM")) + ) + ) + ); ERC1967Proxy llmBridge = new ERC1967Proxy( address(bridgeImpl), abi.encodeCall( @@ -56,7 +72,9 @@ contract Deploy is Script { ) ) ); - llm.transferOwnership(address(llmBridge)); + llm.grantRole(llm.MINTER_ROLE(), address(llmBridge)); + llm.grantRole(llm.PAUSER_ROLE(), address(llmBridge)); + vm.stopBroadcast(); } } diff --git a/eth-bridge/contracts/script/Upgrade.s.sol b/eth-bridge/contracts/script/UpgradeBridges.s.sol similarity index 66% rename from eth-bridge/contracts/script/Upgrade.s.sol rename to eth-bridge/contracts/script/UpgradeBridges.s.sol index 61cf72456..d51804458 100644 --- a/eth-bridge/contracts/script/Upgrade.s.sol +++ b/eth-bridge/contracts/script/UpgradeBridges.s.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.18; import "forge-std/Script.sol"; import "../src/Bridge.sol"; -contract Upgrade is Script { +contract UpgradeBridges is Script { function run() external { vm.startBroadcast(); - Bridge lldProxy = Bridge(vm.envAddress("LLDProxy")); - Bridge llmProxy = Bridge(vm.envAddress("LLMProxy")); + Bridge lldProxy = Bridge(vm.envAddress("LLDBridgeProxy")); + Bridge llmProxy = Bridge(vm.envAddress("LLMBridgeProxy")); Bridge newBridgeImpl = new Bridge(); diff --git a/eth-bridge/contracts/script/UpgradeTokens.s.sol b/eth-bridge/contracts/script/UpgradeTokens.s.sol new file mode 100644 index 000000000..89524bf04 --- /dev/null +++ b/eth-bridge/contracts/script/UpgradeTokens.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Script.sol"; +import "../src/Bridge.sol"; + +contract UpgradeTokens is Script { + function run() external { + vm.startBroadcast(); + Bridge lldBridge = Bridge(vm.envAddress("LLDBridgeProxy")); + Bridge llmBridge = Bridge(vm.envAddress("LLMBridgeProxy")); + WrappedToken lldProxy = lldBridge.token(); + WrappedToken llmProxy = llmBridge.token(); + + WrappedToken newTokenImpl = new WrappedToken(); + + lldProxy.upgradeTo(address(newTokenImpl)); + llmProxy.upgradeTo(address(newTokenImpl)); + } +} diff --git a/eth-bridge/contracts/src/Bridge.sol b/eth-bridge/contracts/src/Bridge.sol index 04e4a4747..5553c2bfc 100644 --- a/eth-bridge/contracts/src/Bridge.sol +++ b/eth-bridge/contracts/src/Bridge.sol @@ -6,31 +6,8 @@ import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/prox import {AccessControlUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; import {WrappedToken} from "./WrappedToken.sol"; - -/// Struct for representing Substrate -> ETH transfer -struct IncomingReceiptStruct { - uint64 substrateBlockNumber; - address ethRecipient; - uint256 amount; - uint256 approvedOn; - uint256 processedOn; -} - -/// Rate limit parameters. Limit how fast can tokens be minted.This is -/// implemented as The Leaky Bucket as a Meter algorithm - -/// https://en.wikipedia.org/wiki/Leaky_bucket. -/// `counterLimit` is the max counter (a.k.a. max burst, max single withdrawal) -/// `decayRate` is after reaching max, how much can be minted per block. -struct RateLimitParameters { - uint256 counterLimit; - uint256 decayRate; -} - -/// Struct for keeping track of counters required to enforce rate limits -struct RateLimitCounter { - uint256 counter; - uint256 lastUpdate; -} +import {IncomingReceiptStruct, RateLimitParameters, RateLimitCounter} from "./BridgeTypes.sol"; +import {BridgeEvents} from "./BridgeEvents.sol"; /// Bridge is deactivated - see `bridgeActive()` and `setActive(bool)` error BridgeInactive(); @@ -59,37 +36,6 @@ error AlreadyVoted(); /// Transferred amount is less than configured minimum error TooSmallAmount(); -/// @title Interface with events emitted by the bridge -interface BridgeEvents { - /// Emitted after burn, notifies relays that transfer is happening - /// @param from Account that burned its tokens - /// @param substrateRecipient Who should get tokens on substrate - /// @param amount Amount of token burned - event OutgoingReceipt(address indexed from, bytes32 indexed substrateRecipient, uint256 amount); - - /// Bridge get activated or deactivated - /// @param newBridgeState New bridge state - event StateChanged(bool newBridgeState); - - /// An IncomingReceipt was approved for transfer. `mint(bytes32)` - /// can now be called for this receipt after `mintDelay` blocks pass. - /// @param receiptId Receipt that got approved - event Approved(bytes32 indexed receiptId); - - /// Vote was cast to approve IncomingReceipt - /// @param receiptId subject Receipt - /// @param relay Relay that cast the vote - event Vote(bytes32 indexed receiptId, address indexed relay, uint64 substrateBlockNumber); - - /// IncomingReceipt was completely processed - tokens were minted - /// @param receiptId subject Receipt - event Processed(bytes32 indexed receiptId); - - /// Bridge was emergency stopped by watcher - misbehavior by relay was - /// detected - event EmergencyStop(); -} - /// @title Substrate <-> ETH bridge for Liberland /// @dev Must be used with ERC1967Proxy contract Bridge is Initializable, AccessControlUpgradeable, UUPSUpgradeable, BridgeEvents { @@ -426,19 +372,6 @@ contract Bridge is Initializable, AccessControlUpgradeable, UUPSUpgradeable, Bri minTransfer = minTransfer_; } - /// Transfer ownership of underlying token contract. - /// Will stop the bridge and set the token address to 0, effectively - /// bricking the bridge. - /// @param newOwner new owner - /// @dev Only addresses with SUPER_ADMIN_ROLE can call this - /// @dev Interacts with `token` contract - function transferTokenOwnership(address newOwner) public onlyRole(SUPER_ADMIN_ROLE) { - WrappedToken token_ = token; - token = WrappedToken(address(0)); - _setActive(false); - token_.transferOwnership(newOwner); - } - /// Check if given receiptId was already voted on by given voter /// @param receiptId ReceiptId to check /// @param voter Voter to check diff --git a/eth-bridge/contracts/src/BridgeEvents.sol b/eth-bridge/contracts/src/BridgeEvents.sol new file mode 100644 index 000000000..41dc0c4c0 --- /dev/null +++ b/eth-bridge/contracts/src/BridgeEvents.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +/// @title Interface with events emitted by the bridge +interface BridgeEvents { + /// Emitted after burn, notifies relays that transfer is happening + /// @param from Account that burned its tokens + /// @param substrateRecipient Who should get tokens on substrate + /// @param amount Amount of token burned + event OutgoingReceipt(address indexed from, bytes32 indexed substrateRecipient, uint256 amount); + + /// Bridge get activated or deactivated + /// @param newBridgeState New bridge state + event StateChanged(bool newBridgeState); + + /// An IncomingReceipt was approved for transfer. `mint(bytes32)` + /// can now be called for this receipt after `mintDelay` blocks pass. + /// @param receiptId Receipt that got approved + event Approved(bytes32 indexed receiptId); + + /// Vote was cast to approve IncomingReceipt + /// @param receiptId subject Receipt + /// @param relay Relay that cast the vote + event Vote(bytes32 indexed receiptId, address indexed relay, uint64 substrateBlockNumber); + + /// IncomingReceipt was completely processed - tokens were minted + /// @param receiptId subject Receipt + event Processed(bytes32 indexed receiptId); + + /// Bridge was emergency stopped by watcher - misbehavior by relay was + /// detected + event EmergencyStop(); +} diff --git a/eth-bridge/contracts/src/BridgeTypes.sol b/eth-bridge/contracts/src/BridgeTypes.sol new file mode 100644 index 000000000..c6869ec06 --- /dev/null +++ b/eth-bridge/contracts/src/BridgeTypes.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +/// Struct for representing Substrate -> ETH transfer +struct IncomingReceiptStruct { + uint64 substrateBlockNumber; + address ethRecipient; + uint256 amount; + uint256 approvedOn; + uint256 processedOn; +} + +/// Rate limit parameters. Limit how fast can tokens be minted.This is +/// implemented as The Leaky Bucket as a Meter algorithm - +/// https://en.wikipedia.org/wiki/Leaky_bucket. +/// `counterLimit` is the max counter (a.k.a. max burst, max single withdrawal) +/// `decayRate` is after reaching max, how much can be minted per block. +struct RateLimitParameters { + uint256 counterLimit; + uint256 decayRate; +} + +/// Struct for keeping track of counters required to enforce rate limits +struct RateLimitCounter { + uint256 counter; + uint256 lastUpdate; +} diff --git a/eth-bridge/contracts/src/WrappedToken.sol b/eth-bridge/contracts/src/WrappedToken.sol index f8ed099d0..8e87d8635 100644 --- a/eth-bridge/contracts/src/WrappedToken.sol +++ b/eth-bridge/contracts/src/WrappedToken.sol @@ -1,34 +1,61 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; -import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {Pausable} from "openzeppelin-contracts/contracts/security/Pausable.sol"; -import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import {ERC20Permit} from "openzeppelin-contracts/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; +import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {AccessControlUpgradeable} from + "openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol"; +import {ERC20Upgradeable} from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; +import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; +import {ERC20PermitUpgradeable} from + "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; -contract WrappedToken is ERC20, Pausable, Ownable, ERC20Permit { - // solhint-disable-next-line no-empty-blocks - constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} +/// @dev Must be used with ERC1967Proxy +contract WrappedToken is + Initializable, + ERC20Upgradeable, + PausableUpgradeable, + AccessControlUpgradeable, + ERC20PermitUpgradeable, + UUPSUpgradeable +{ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + constructor() { + _disableInitializers(); + } + + function initialize(string memory name, string memory symbol) public initializer { + __ERC20_init(name, symbol); + __Pausable_init(); + __AccessControl_init(); + __ERC20Permit_init(name); + __UUPSUpgradeable_init(); - function pause() public onlyOwner { + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function pause() public onlyRole(PAUSER_ROLE) { _pause(); } - function unpause() public onlyOwner { + function unpause() public onlyRole(PAUSER_ROLE) { _unpause(); } - function mint(address to, uint256 amount) public onlyOwner { + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { _mint(to, amount); } - function burn(address account, uint256 amount) public onlyOwner { + function burn(address account, uint256 amount) public onlyRole(MINTER_ROLE) { _spendAllowance(account, _msgSender(), amount); _burn(account, amount); } function allowance(address owner, address spender) public view virtual override returns (uint256) { - if (spender == super.owner()) { + if (hasRole(MINTER_ROLE, spender)) { return type(uint256).max; } return super.allowance(owner, spender); @@ -41,4 +68,7 @@ contract WrappedToken is ERC20, Pausable, Ownable, ERC20Permit { function _beforeTokenTransfer(address from, address to, uint256 amount) internal override whenNotPaused { super._beforeTokenTransfer(from, to, amount); } + + // solhint-disable-next-line no-empty-blocks + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} } diff --git a/eth-bridge/contracts/test/Bridge.t.sol b/eth-bridge/contracts/test/Bridge.t.sol index db196335a..91cb6af1e 100644 --- a/eth-bridge/contracts/test/Bridge.t.sol +++ b/eth-bridge/contracts/test/Bridge.t.sol @@ -25,25 +25,36 @@ contract BridgeTest is Test, BridgeEvents { event Transfer(address indexed from, address indexed to, uint256 value); function setUp() public { - Bridge impl = new Bridge(); - token = new WrappedToken("Liberland Merits", "LLM"); - bridge = Bridge( + Bridge bridgeImpl = new Bridge(); + WrappedToken tokenImpl = new WrappedToken(); + token = WrappedToken( address( new ERC1967Proxy( - address(impl), - abi.encodeCall( - Bridge.initialize, - ( - token, - 2, - 10, - 4, - 1000, - 10, - 650, - 0 - ) + address(tokenImpl), + abi.encodeCall( + WrappedToken.initialize, + ("Liberland Merits", "LLM") + ) ) + ) + ); + bridge = Bridge( + address( + new ERC1967Proxy( + address(bridgeImpl), + abi.encodeCall( + Bridge.initialize, + ( + token, + 2, + 10, + 4, + 1000, + 10, + 650, + 0 + ) + ) ) ) ); @@ -54,13 +65,15 @@ contract BridgeTest is Test, BridgeEvents { bridge.grantRole(bridge.WATCHER_ROLE(), alice); bridge.grantRole(bridge.WATCHER_ROLE(), dave); vm.deal(alice, 100); + token.grantRole(token.MINTER_ROLE(), address(bridge)); + token.grantRole(token.PAUSER_ROLE(), address(bridge)); + vm.startPrank(address(bridge)); token.mint(alice, 100); token.mint(bob, 100); token.mint(charlie, 100); token.mint(dave, 100); token.mint(address(this), 100); - token.transferOwnership(address(bridge)); - token.approve(address(bridge), 9999999); + vm.stopPrank(); vm.prank(dave); bridge.setActive(true); @@ -804,28 +817,6 @@ contract BridgeTest is Test, BridgeEvents { bridge.fee(); } - function testOnlySuperAdminCanTransferTokenOwnership() public { - vm.prank(alice); - vm.expectRevert( - "AccessControl: account 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf is missing role 0x7613a25ecc738585a232ad50a301178f12b3ba8887d13e138b523c4269c47689" - ); - bridge.transferTokenOwnership(alice); - } - - function testTokenOwnershipIsTransferable() public { - bridge.transferTokenOwnership(address(this)); - assertEq(token.owner(), address(this)); - } - - function testTokenTransferBricksBridge() public { - bridge.transferTokenOwnership(address(this)); - assertEq(bridge.bridgeActive(), false); - - vm.prank(dave); - vm.expectRevert(InvalidConfiguration.selector); - bridge.setActive(true); - } - function testClaimRewardWorks() public { vm.prank(alice); bridge.voteMint(receipt1, 1, 100, dave); diff --git a/eth-bridge/contracts/test/VoteFeeCompare.t.sol b/eth-bridge/contracts/test/VoteFeeCompare.t.sol index ec00bd21c..309842b1a 100644 --- a/eth-bridge/contracts/test/VoteFeeCompare.t.sol +++ b/eth-bridge/contracts/test/VoteFeeCompare.t.sol @@ -25,25 +25,36 @@ contract BridgeTest is Test, BridgeEvents { event Transfer(address indexed from, address indexed to, uint256 value); function setUp() public { - Bridge impl = new Bridge(); - token = new WrappedToken("Liberland Merits", "LLM"); - bridge = Bridge( + Bridge bridgeImpl = new Bridge(); + WrappedToken tokenImpl = new WrappedToken(); + token = WrappedToken( address( new ERC1967Proxy( - address(impl), - abi.encodeCall( - Bridge.initialize, - ( - token, - 3, - 0, - 0, - 0, - 0, - 100, - 0 - ) + address(tokenImpl), + abi.encodeCall( + WrappedToken.initialize, + ("Liberland Merits", "LLM") + ) ) + ) + ); + bridge = Bridge( + address( + new ERC1967Proxy( + address(bridgeImpl), + abi.encodeCall( + Bridge.initialize, + ( + token, + 3, + 10, + 4, + 1000, + 10, + 650, + 0 + ) + ) ) ) ); @@ -53,8 +64,8 @@ contract BridgeTest is Test, BridgeEvents { bridge.grantRole(bridge.RELAY_ROLE(), charlie); bridge.grantRole(bridge.RELAY_ROLE(), dave); bridge.grantRole(bridge.RELAY_ROLE(), address(this)); - token.transferOwnership(address(bridge)); - token.approve(address(bridge), 9999999); + token.grantRole(token.MINTER_ROLE(), address(bridge)); + token.grantRole(token.PAUSER_ROLE(), address(bridge)); bridge.setActive(true); } @@ -79,6 +90,9 @@ contract BridgeTest is Test, BridgeEvents { bridge.voteMint(receipt1, 1, 100, alice); approve = gas - gasleft(); + console.log(standard); + console.log(first); + console.log(approve); assert(standard < first); assert(standard < approve); diff --git a/eth-bridge/contracts/test/WrappedToken.t.sol b/eth-bridge/contracts/test/WrappedToken.t.sol index 3e315eec2..ec760cf86 100644 --- a/eth-bridge/contracts/test/WrappedToken.t.sol +++ b/eth-bridge/contracts/test/WrappedToken.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import "forge-std/Test.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../src/WrappedToken.sol"; using stdStorage for StdStorage; @@ -14,12 +15,25 @@ contract WrappedTokenTest is Test { address charlie = vm.addr(3); function setUp() public { - token = new WrappedToken("Liberland Merits", "LLM"); + WrappedToken tokenImpl = new WrappedToken(); + token = WrappedToken( + address( + new ERC1967Proxy( + address(tokenImpl), + abi.encodeCall( + WrappedToken.initialize, + ("Liberland Merits", "LLM") + ) + ) + ) + ); + token.grantRole(token.MINTER_ROLE(), alice); + token.grantRole(token.PAUSER_ROLE(), alice); + vm.prank(alice); token.mint(bob, 10); - token.transferOwnership(alice); } - function testOwnerNeedsNoApproval() public { + function testMinterNeedsNoApproval() public { vm.startPrank(alice); token.transferFrom(bob, alice, 1); token.burn(bob, 1); @@ -44,4 +58,84 @@ contract WrappedTokenTest is Test { vm.prank(charlie); token.transferFrom(bob, alice, 2); } + + function testPauserCanPause() public { + vm.prank(alice); + token.pause(); + vm.prank(alice); + token.unpause(); + } + + function testOthersCantPause() public { + vm.prank(bob); + vm.expectRevert( + "AccessControl: account 0x2b5ad5c4795c026514f8317c7a215e218dccd6cf is missing role 0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a" + ); + token.pause(); + + vm.prank(alice); + token.pause(); + + vm.prank(bob); + vm.expectRevert( + "AccessControl: account 0x2b5ad5c4795c026514f8317c7a215e218dccd6cf is missing role 0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a" + ); + token.unpause(); + } + + function testMinterCanMintAndBurn() public { + vm.prank(alice); + token.mint(bob, 10); + + vm.prank(alice); + token.burn(bob, 10); + } + + function testOthersCantMintNorBurn() public { + vm.prank(bob); + vm.expectRevert( + "AccessControl: account 0x2b5ad5c4795c026514f8317c7a215e218dccd6cf is missing role 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6" + ); + token.mint(bob, 10); + + vm.prank(alice); + token.mint(bob, 10); + + vm.prank(bob); + vm.expectRevert( + "AccessControl: account 0x2b5ad5c4795c026514f8317c7a215e218dccd6cf is missing role 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6" + ); + token.burn(bob, 10); + } + + function testOnlyUpgraderCanUpgrade() public { + token.grantRole(token.UPGRADER_ROLE(), address(this)); + + WrappedToken impl2 = new WrappedToken(); + + vm.prank(bob); + vm.expectRevert( + "AccessControl: account 0x2b5ad5c4795c026514f8317c7a215e218dccd6cf is missing role 0x189ab7a9244df0848122154315af71fe140f3db0fe014031783b0946b8c9d2e3" + ); + token.upgradeTo(address(impl2)); + + token.upgradeTo(address(impl2)); + } + + function testOnlyUpgradesToUUPSCompatible() public { + token.grantRole(token.UPGRADER_ROLE(), address(this)); + + vm.expectRevert("ERC1967Upgrade: new implementation is not UUPS"); + token.upgradeTo(address(token)); + } + + function testIsUpgradeable() public { + token.grantRole(token.UPGRADER_ROLE(), address(this)); + + WrappedToken impl2 = new WrappedToken(); + token.upgradeTo(address(impl2)); + + vm.expectCall(address(impl2), ""); + token.balanceOf(alice); + } }