Skip to content

Commit

Permalink
Upgradeable ERC-20 Tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
kacperzuk-neti committed Aug 18, 2023
1 parent 2548cc5 commit 00e82c9
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 119 deletions.
42 changes: 26 additions & 16 deletions eth-bridge/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion eth-bridge/contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.18"
solc_version = "0.8.20"

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
2 changes: 1 addition & 1 deletion eth-bridge/contracts/lib/openzeppelin-contracts
28 changes: 23 additions & 5 deletions eth-bridge/contracts/script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/Bridge.sol";

contract Upgrade 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();

Expand Down
20 changes: 20 additions & 0 deletions eth-bridge/contracts/script/UpgradeTokens.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/Bridge.sol";

contract Upgrade 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));
}
}
15 changes: 1 addition & 14 deletions eth-bridge/contracts/src/Bridge.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
pragma solidity ^0.8.20;

import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
Expand Down Expand Up @@ -426,19 +426,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
Expand Down
55 changes: 42 additions & 13 deletions eth-bridge/contracts/src/WrappedToken.sol
Original file line number Diff line number Diff line change
@@ -1,34 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
pragma solidity ^0.8.20;

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) {}
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);
Expand All @@ -41,4 +67,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) {}
}
73 changes: 32 additions & 41 deletions eth-bridge/contracts/test/Bridge.t.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
Expand All @@ -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
)
)
)
)
);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 00e82c9

Please sign in to comment.