Skip to content

Commit 49763aa

Browse files
authored
WstEth L2 Token (#11)
* forge install: openzeppelin-contracts-upgradeable v5.0.2 * Fix WstEthL2Token * initial token tests * forge install: openzeppelin-foundry-upgrades v0.3.1 * Add token tests * Isolate token logic changes * Fixed tests * Run formatter * Respond to review comments: - remove unnecessary modifiers, use INttToken interface, clean up tests * Updates: fix minter test, add comment about UUPSUpgradeable initializer * Undo MockGateway comment out * Remove INTTToken file and wstETHL2Token old file * Fix Axelar transceiver tests * Run formatter
1 parent 43e425c commit 49763aa

13 files changed

+521
-59
lines changed

.gitmodules

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,12 @@
1616
[submodule "lib/wormhole-solidity-sdk"]
1717
path = lib/wormhole-solidity-sdk
1818
url = https://github.com/wormhole-foundation/wormhole-solidity-sdk
19+
[submodule "lib/axelar-cgp-solidity"]
20+
path = lib/axelar-cgp-solidity
21+
url = https://github.com/axelarnetwork/axelar-cgp-solidity
22+
[submodule "lib/openzeppelin-contracts-upgradeable"]
23+
path = lib/openzeppelin-contracts-upgradeable
24+
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
25+
[submodule "lib/openzeppelin-foundry-upgrades"]
26+
path = lib/openzeppelin-foundry-upgrades
27+
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades

foundry.toml

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
[profile.default]
2-
solc_version = "0.8.23"
3-
optimizer = true
4-
optimizer_runs = 200
5-
via_ir = false
6-
evm_version = "london"
7-
src = "src"
8-
out = "out"
9-
libs = ["lib"]
10-
fs_permissions = [{ access = "read", path = "./test/payloads"}]
2+
build_info = true
3+
evm_version = "paris"
4+
extra_output = ["storageLayout"]
5+
ffi = true
6+
libs = ["lib"]
7+
optimizer = true
8+
optimizer_runs = 200
9+
out = "out"
10+
solc_version = "0.8.23"
11+
src = "src"
12+
via_ir = false
1113

12-
[profile.prod]
13-
via_ir = true
14-
fs_permissions = [{access = "read", path = "./cfg/"}]
14+
[profile.ci]
15+
fuzz = { runs = 256 }
16+
invariant = { runs = 1000 }
17+
via_ir = true
18+
19+
[rpc_endpoints]
20+
bsc = "${BSC_RPC_URL}"
21+
mainnet = "${MAINNET_RPC_URL}"
1522

1623
[fmt]
17-
line_length = 100
18-
multiline_func_header = "params_first"
19-
# wrap_comments = true
24+
line_length = 100
25+
multiline_func_header = "params_first"
26+
# wrap_comments = true
27+
28+
[profile.production]
29+
via_ir = true
2030

21-
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
31+
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

lib/openzeppelin-foundry-upgrades

remappings.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
@axelar-network/axelar-gmp-sdk-solidity=lib/axelar-gmp-sdk-solidity/
22
@wormhole-foundation/native_token_transfer=lib/example-native-token-transfers/evm/src
3+
@openzeppelin/foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/

script/lib/Upgrades.sol

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-License-Identifier: Apache 2
2+
// slither-disable-start reentrancy-benign
3+
pragma solidity >=0.8.8 <0.9.0;
4+
5+
import {Vm} from "forge-std/Vm.sol";
6+
import {Upgrades as OzUpgrades, Options} from "@openzeppelin/foundry-upgrades/Upgrades.sol";
7+
import {DefenderDeploy} from "@openzeppelin/foundry-upgrades/internal/DefenderDeploy.sol";
8+
9+
// Much of the code in this file was reused from https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/blob/c50a7968d369f852607cb72e653d1ed699d823c5/src/Upgrades.sol.
10+
library Upgrades {
11+
address constant CHEATCODE_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D;
12+
13+
function deployUUPSProxy(
14+
string memory proxyContractPath,
15+
string memory contractName,
16+
bytes memory initializerData
17+
) internal returns (address) {
18+
Options memory opts;
19+
address impl = OzUpgrades.deployImplementation(contractName, opts);
20+
return address(_deploy(proxyContractPath, abi.encode(impl, initializerData), opts));
21+
}
22+
23+
function _deploy(
24+
string memory contractName,
25+
bytes memory constructorData,
26+
Options memory opts
27+
) private returns (address) {
28+
if (opts.defender.useDefenderDeploy) {
29+
return DefenderDeploy.deploy(contractName, constructorData, opts.defender);
30+
} else {
31+
bytes memory creationCode = Vm(CHEATCODE_ADDRESS).getCode(contractName);
32+
address deployedAddress =
33+
_deployFromBytecode(abi.encodePacked(creationCode, constructorData));
34+
if (deployedAddress == address(0)) {
35+
revert(
36+
string.concat(
37+
"Failed to deploy contract ",
38+
contractName,
39+
' using constructor data "',
40+
string(constructorData),
41+
'"'
42+
)
43+
);
44+
}
45+
return deployedAddress;
46+
}
47+
}
48+
49+
function _deployFromBytecode(bytes memory bytecode) private returns (address) {
50+
address addr;
51+
assembly {
52+
addr := create(0, add(bytecode, 32), mload(bytecode))
53+
}
54+
return addr;
55+
}
56+
}

src/interfaces/external/INTTToken.sol

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/token/WstEthL2Token.sol

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: Apache 2
2+
pragma solidity >=0.8.8 <0.9.0;
3+
4+
import {OwnableUpgradeable} from
5+
"openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
6+
import {ERC20Upgradeable} from
7+
"openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
8+
import {ERC20BurnableUpgradeable} from
9+
"openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
10+
import {UUPSUpgradeable} from
11+
"openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
12+
import {INttToken} from "@wormhole-foundation/native_token_transfer/interfaces/INttToken.sol";
13+
14+
/// @title WstEthL2Token
15+
/// @notice A UUPS upgradeable token with access controlled minting and burning.
16+
contract WstEthL2Token is
17+
INttToken,
18+
UUPSUpgradeable,
19+
ERC20Upgradeable,
20+
ERC20BurnableUpgradeable,
21+
OwnableUpgradeable
22+
{
23+
// =============== Storage ==============================================================
24+
25+
struct MinterStorage {
26+
address _minter;
27+
}
28+
29+
bytes32 private constant MINTER_SLOT = bytes32(uint256(keccak256("wsteth.minter")) - 1);
30+
31+
// =============== Storage Getters/Setters ==============================================
32+
33+
function _getMinterStorage() internal pure returns (MinterStorage storage $) {
34+
uint256 slot = uint256(MINTER_SLOT);
35+
assembly ("memory-safe") {
36+
$.slot := slot
37+
}
38+
}
39+
40+
/// @notice A function to set the new minter for the tokens.
41+
/// @param newMinter The address to add as both a minter and burner.
42+
function setMinter(address newMinter) external onlyOwner {
43+
if (newMinter == address(0)) {
44+
revert InvalidMinterZeroAddress();
45+
}
46+
address previousMinter = _getMinterStorage()._minter;
47+
_getMinterStorage()._minter = newMinter;
48+
emit NewMinter(previousMinter, newMinter);
49+
}
50+
51+
/// @dev Returns the address of the current minter.
52+
function minter() public view returns (address) {
53+
MinterStorage storage $ = _getMinterStorage();
54+
return $._minter;
55+
}
56+
57+
/// @dev Throws if called by any account other than the minter.
58+
modifier onlyMinter() {
59+
if (minter() != _msgSender()) {
60+
revert CallerNotMinter(_msgSender());
61+
}
62+
_;
63+
}
64+
65+
/// @dev An error thrown when a method is not implemented.
66+
error UnimplementedMethod();
67+
68+
/// @custom:oz-upgrades-unsafe-allow constructor
69+
constructor() {
70+
_disableInitializers();
71+
}
72+
73+
/// @notice A one-time configuration method meant to be called immediately upon the deployment of `WstEthL2Token`. It sets
74+
/// up the token's name, symbol, and owner
75+
function initialize(
76+
string memory _name,
77+
string memory _symbol,
78+
address _owner
79+
) external initializer {
80+
// OpenZeppelin upgradeable contracts documentation says:
81+
//
82+
// "Use with multiple inheritance requires special care. Initializer
83+
// functions are not linearized by the compiler like constructors.
84+
// Because of this, each __{ContractName}_init function embeds the
85+
// linearized calls to all parent initializers. As a consequence,
86+
// calling two of these init functions can potentially initialize the
87+
// same contract twice."
88+
//
89+
// Note that ERC20 extensions do not linearize calls to ERC20Upgradeable
90+
// initializer so we call all extension initializers individually.
91+
__ERC20_init(_name, _symbol);
92+
__Ownable_init(_owner);
93+
94+
// These initializers don't do anything, so we won't call them
95+
// __ERC20Burnable_init();
96+
// __UUPSUpgradeable_init();
97+
}
98+
99+
/// @notice A function that will burn tokens held by the `msg.sender`.
100+
/// @param _value The amount of tokens to be burned.
101+
function burn(uint256 _value) public override(INttToken, ERC20BurnableUpgradeable) onlyMinter {
102+
ERC20BurnableUpgradeable.burn(_value);
103+
}
104+
105+
/// @notice This method is not implemented and should not be called.
106+
function burnFrom(address, uint256) public pure override {
107+
revert UnimplementedMethod();
108+
}
109+
110+
/// @notice A function that mints new tokens to a specific account.
111+
/// @param _account The address where new tokens will be minted.
112+
/// @param _amount The amount of new tokens that will be minted.
113+
function mint(address _account, uint256 _amount) external onlyMinter {
114+
_mint(_account, _amount);
115+
}
116+
117+
function _authorizeUpgrade(address /* newImplementation */ ) internal view override onlyOwner {}
118+
}

src/token/wstETHL2Token.sol

Lines changed: 0 additions & 32 deletions
This file was deleted.

test/axelar/AxelarTransceiver.t.sol

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {NttManager} from "@wormhole-foundation/native_token_transfer/NttManager/
1010
import {INttManager} from "@wormhole-foundation/native_token_transfer/interfaces/INttManager.sol";
1111
import {IManagerBase} from "@wormhole-foundation/native_token_transfer/interfaces/IManagerBase.sol";
1212
import {ERC1967Proxy} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
13-
import {wstETHL2Token} from "../../src/token/wstETHL2Token.sol";
13+
import {WstEthL2Token} from "src/token/WstEthL2Token.sol";
14+
import {WstEthL2TokenHarness} from "test/token/WstEthL2TokenHarness.sol";
15+
import {Upgrades} from "script/lib/Upgrades.sol";
1416

1517
import "forge-std/console.sol";
1618
import "forge-std/Test.sol";
@@ -29,7 +31,7 @@ contract AxelarTransceiverTest is Test {
2931
IAxelarGateway gateway;
3032
IAxelarGasService gasService;
3133
NttManager manager;
32-
wstETHL2Token token;
34+
WstEthL2TokenHarness token;
3335

3436
function setUp() public {
3537
string memory url = "https://ethereum-sepolia-rpc.publicnode.com";
@@ -38,7 +40,16 @@ contract AxelarTransceiverTest is Test {
3840
gateway = IAxelarGateway(new MockAxelarGateway());
3941
gasService = IAxelarGasService(address(new MockAxelarGasService()));
4042

41-
token = new wstETHL2Token("name", "symobl", OWNER, OWNER);
43+
// Deploy the token
44+
address proxy = Upgrades.deployUUPSProxy(
45+
"out/ERC1967Proxy.sol/ERC1967Proxy.json",
46+
"WstEthL2TokenHarness.sol",
47+
abi.encodeCall(WstEthL2Token.initialize, ("Wrapped Staked Eth", "wstEth", OWNER))
48+
);
49+
vm.label(proxy, "Proxy");
50+
51+
token = WstEthL2TokenHarness(proxy);
52+
4253
address managerImplementation = address(
4354
new NttManager(
4455
address(token),
@@ -187,6 +198,8 @@ contract AxelarTransceiverTest is Test {
187198
vm.prank(OWNER);
188199
transceiver.setAxelarChainId(chainId, chainName, axelarAddress);
189200
vm.prank(OWNER);
201+
token.setMinter(OWNER);
202+
vm.prank(OWNER);
190203
token.mint(address(manager), amount);
191204

192205
transceiver.execute(bytes32(0), chainName, axelarAddress, payload);

0 commit comments

Comments
 (0)