diff --git a/foundry.toml b/foundry.toml index 283e5cb..4ef445b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,5 +18,6 @@ runs = 10000 [rpc_endpoints] sepolia = "https://ethereum-sepolia.publicnode.com" mainnet = "https://ethereum-rpc.publicnode.com" +base = "https://base-rpc.publicnode.com" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/script/DeployStakedAvailWormhole.s.sol b/script/DeployStakedAvailWormhole.s.sol new file mode 100644 index 0000000..49826c4 --- /dev/null +++ b/script/DeployStakedAvailWormhole.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.25; + +import {TransparentUpgradeableProxy} from + "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {StakedAvailWormhole} from "src/StakedAvailWormhole.sol"; +import {Script} from "forge-std/Script.sol"; + +contract Deploy is Script { + function run() external { + vm.startBroadcast(); + address admin = vm.envAddress("ADMIN"); + address impl = address(new StakedAvailWormhole()); + StakedAvailWormhole stavail = StakedAvailWormhole(address(new TransparentUpgradeableProxy(impl, admin, ""))); + stavail.initialize(admin); + vm.stopBroadcast(); + } +} diff --git a/src/StakedAvailWormhole.sol b/src/StakedAvailWormhole.sol new file mode 100644 index 0000000..2631d54 --- /dev/null +++ b/src/StakedAvailWormhole.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.25; + +import { + ERC20Upgradeable, + ERC20PermitUpgradeable +} from "lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {AccessControlDefaultAdminRulesUpgradeable} from + "lib/openzeppelin-contracts-upgradeable/contracts/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; +import {INttToken} from "src/interfaces/INttToken.sol"; + +/// @title StakedAvail +/// @author Deq Protocol +/// @title Staked Avail ERC20 token with support for Wormhole +/// @notice A Staked Avail token implementation for Wormhole-based bridges +contract StakedAvailWormhole is AccessControlDefaultAdminRulesUpgradeable, ERC20PermitUpgradeable, INttToken { + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + constructor() { + _disableInitializers(); + } + + function initialize(address governance) external initializer { + __ERC20Permit_init("Staked Avail (Wormhole)"); + __ERC20_init("Staked Avail (Wormhole)", "stAVAIL.W"); + __AccessControlDefaultAdminRules_init(0, governance); + } + + function mint(address account, uint256 amount) external onlyRole(MINTER_ROLE) { + _mint(account, amount); + } + + function burn(uint256 amount) external { + _burn(msg.sender, amount); + } +} diff --git a/src/interfaces/INttToken.sol b/src/interfaces/INttToken.sol new file mode 100644 index 0000000..ccb1c82 --- /dev/null +++ b/src/interfaces/INttToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.25; + +interface INttToken { + // NOTE: the `mint` method is not present in the standard ERC20 interface. + function mint(address account, uint256 amount) external; + + // NOTE: NttTokens in `burn` mode require the `burn` method to be present. + // This method is not present in the standard ERC20 interface, but is + // found in the `ERC20Burnable` interface. + function burn(uint256 amount) external; +} diff --git a/test/StakedAvailWormhole.t.sol b/test/StakedAvailWormhole.t.sol new file mode 100644 index 0000000..d7ef2d4 --- /dev/null +++ b/test/StakedAvailWormhole.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.25; + +import {StakedAvailWormhole} from "src/StakedAvailWormhole.sol"; +import {TransparentUpgradeableProxy} from + "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IAccessControl} from "lib/openzeppelin-contracts/contracts/access/IAccessControl.sol"; +import {Vm, Test} from "forge-std/Test.sol"; + +contract StakedAvailWormholeTest is Test { + StakedAvailWormhole stavail; + address owner; + address governance; + address minter; + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + function setUp() external { + governance = makeAddr("governance"); + minter = makeAddr("minter"); + address impl = address(new StakedAvailWormhole()); + stavail = StakedAvailWormhole(address(new TransparentUpgradeableProxy(impl, msg.sender, ""))); + stavail.initialize(governance); + vm.prank(governance); + stavail.grantRole(MINTER_ROLE, minter); + } + + function testRevert_initialize(address rand) external { + vm.expectRevert(); + stavail.initialize(rand); + } + + function test_initialize() external view { + assertEq(stavail.totalSupply(), 0); + assertNotEq(stavail.owner(), address(0)); + assertEq(stavail.owner(), governance); + assertNotEq(stavail.name(), ""); + assertEq(stavail.name(), "Staked Avail (Wormhole)"); + assertNotEq(stavail.symbol(), ""); + assertEq(stavail.symbol(), "stAVAIL.W"); + } + + function testRevertOnlyMinter_mint(address to, uint256 amount) external { + address rand = makeAddr("rand"); + vm.assume(rand != minter); + vm.expectRevert( + abi.encodeWithSelector((IAccessControl.AccessControlUnauthorizedAccount.selector), rand, MINTER_ROLE) + ); + vm.prank(rand); + stavail.mint(to, amount); + } + + function test_mint(address to, uint256 amount) external { + vm.assume(to != address(0)); + vm.prank(minter); + stavail.mint(to, amount); + assertEq(stavail.balanceOf(to), amount); + } + + function test_burn(address from, uint256 amount) external { + vm.assume(from != address(0)); + vm.prank(minter); + stavail.mint(from, amount); + assertEq(stavail.balanceOf(from), amount); + vm.prank(from); + stavail.burn(amount); + assertEq(stavail.balanceOf(from), 0); + } + + function test_burn2(address from, uint256 amount, uint256 amount2) external { + vm.assume(from != address(0) && amount2 < amount); + vm.prank(minter); + stavail.mint(from, amount); + assertEq(stavail.balanceOf(from), amount); + vm.prank(from); + stavail.burn(amount2); + assertEq(stavail.balanceOf(from), amount - amount2); + } +}