first commit
This commit is contained in:
commit
27d859116e
|
@ -0,0 +1,18 @@
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Hardhat files
|
||||||
|
/cache
|
||||||
|
/artifacts
|
||||||
|
|
||||||
|
# TypeChain files
|
||||||
|
/typechain
|
||||||
|
/typechain-types
|
||||||
|
|
||||||
|
# solidity-coverage files
|
||||||
|
/coverage
|
||||||
|
/coverage.json
|
||||||
|
|
||||||
|
# Hardhat Ignition default folder for deployments against a local node
|
||||||
|
ignition/deployments/chain-31337
|
|
@ -0,0 +1,21 @@
|
||||||
|
# 10Grans Token
|
||||||
|
|
||||||
|
EVM-compatible ERC-20 token with extra features, capable of being bridged to an
|
||||||
|
Optimism-compatible L2. Built for Base blockchain and the 10Grans,
|
||||||
|
but general enough to be reused.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
APP_ENV=development npx hardhat compile
|
||||||
|
|
||||||
|
cp env.example .env.development
|
||||||
|
# (customize your env file here first)
|
||||||
|
|
||||||
|
scripts/deploy-dev.sh
|
||||||
|
|
||||||
|
# optionally verify your contract on L2, customize the following:
|
||||||
|
APP_ENV=development npx hardhat verify --network mainnet 0xl2tokenaddress MrTestToken TEST 0xbridgeonl2 0xl1tokenaddress
|
||||||
|
```
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
MIT license.
|
|
@ -0,0 +1,111 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Compatible with OpenZeppelin Contracts ^5.0.0
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import {ERC20FlashMint} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol";
|
||||||
|
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
|
||||||
|
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
||||||
|
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
|
||||||
|
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
|
||||||
|
|
||||||
|
import { IERC7802, IERC165 } from "./interfaces/IERC7802.sol";
|
||||||
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
|
||||||
|
import { Predeploys } from "./libraries/SuperChainPredeploys.sol";
|
||||||
|
import { TransitionMintLib } from "./libraries/TransitionMintLib.sol";
|
||||||
|
|
||||||
|
/// @notice Error for an unauthorized CALLER.
|
||||||
|
error Unauthorized();
|
||||||
|
|
||||||
|
/// @notice Error for an invalid signature.
|
||||||
|
error InvalidSignature();
|
||||||
|
|
||||||
|
/// @notice Error for an already minted address.
|
||||||
|
error AlreadyMinted();
|
||||||
|
|
||||||
|
/// @notice Error for an invalid chain.
|
||||||
|
error InvalidChain();
|
||||||
|
|
||||||
|
/// @notice Error for an invalid initial supply.
|
||||||
|
error InvalidInitialSupply();
|
||||||
|
|
||||||
|
/// @custom:security-contact moon.eth
|
||||||
|
contract TenGransToken is ERC20, ERC20Permit, ERC20Votes, ERC20FlashMint, ERC20Capped, IERC7802 {
|
||||||
|
address public signer;
|
||||||
|
mapping(address => bool) public transitionMinted;
|
||||||
|
|
||||||
|
event TransitionMint(address indexed holder, uint256 amount);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
string memory name,
|
||||||
|
string memory symbol,
|
||||||
|
uint256 initialSupplyWei,
|
||||||
|
uint256 capWei,
|
||||||
|
address _signer
|
||||||
|
) ERC20(name, symbol) ERC20Permit(name) ERC20Capped(capWei) {
|
||||||
|
require(initialSupplyWei <= capWei, InvalidInitialSupply());
|
||||||
|
_mint(msg.sender, initialSupplyWei);
|
||||||
|
signer = _signer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Allows the SuperchainTokenBridge to mint tokens.
|
||||||
|
/// @param _to Address to mint tokens to.
|
||||||
|
/// @param _amount Amount of tokens to mint.
|
||||||
|
function crosschainMint(address _to, uint256 _amount) external {
|
||||||
|
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
|
||||||
|
|
||||||
|
_mint(_to, _amount);
|
||||||
|
|
||||||
|
emit CrosschainMint(_to, _amount, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Allows the SuperchainTokenBridge to burn tokens.
|
||||||
|
/// @param _from Address to burn tokens from.
|
||||||
|
/// @param _amount Amount of tokens to burn.
|
||||||
|
function crosschainBurn(address _from, uint256 _amount) external {
|
||||||
|
if (msg.sender != Predeploys.SUPERCHAIN_TOKEN_BRIDGE) revert Unauthorized();
|
||||||
|
|
||||||
|
_burn(_from, _amount);
|
||||||
|
|
||||||
|
emit CrosschainBurn(_from, _amount, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc IERC165
|
||||||
|
function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) {
|
||||||
|
return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId
|
||||||
|
|| _interfaceId == type(IERC165).interfaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Allows a wallet to mint tokens using a signature, based on the snapshot quantity.
|
||||||
|
/// @param holder Address to mint tokens to.
|
||||||
|
/// @param snapshotQuantity Amount of tokens to mint.
|
||||||
|
/// @param signature Signature of the holder.
|
||||||
|
function transitionMint(address holder, uint256 snapshotQuantity, bytes memory signature) external {
|
||||||
|
require(block.chainid == 8453, InvalidChain());
|
||||||
|
require(!transitionMinted[holder], AlreadyMinted());
|
||||||
|
require(TransitionMintLib.verifyMintSignature(holder, snapshotQuantity, signature, signer), InvalidSignature());
|
||||||
|
|
||||||
|
transitionMinted[holder] = true;
|
||||||
|
_mint(holder, snapshotQuantity);
|
||||||
|
emit TransitionMint(holder, snapshotQuantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following functions are overrides required by Solidity.
|
||||||
|
|
||||||
|
function _update(address from, address to, uint256 value)
|
||||||
|
internal
|
||||||
|
override(ERC20, ERC20Votes, ERC20Capped)
|
||||||
|
{
|
||||||
|
super._update(from, to, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonces(address owner)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
override(ERC20Permit, Nonces)
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
|
return super.nonces(owner);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol";
|
||||||
|
|
||||||
|
/// @title IERC7802
|
||||||
|
/// @notice Defines the interface for crosschain ERC20 transfers.
|
||||||
|
interface IERC7802 is IERC165 {
|
||||||
|
/// @notice Emitted when a crosschain transfer mints tokens.
|
||||||
|
/// @param to Address of the account tokens are being minted for.
|
||||||
|
/// @param amount Amount of tokens minted.
|
||||||
|
/// @param sender Address of the account that finilized the crosschain transfer.
|
||||||
|
event CrosschainMint(address indexed to, uint256 amount, address indexed sender);
|
||||||
|
|
||||||
|
/// @notice Emitted when a crosschain transfer burns tokens.
|
||||||
|
/// @param from Address of the account tokens are being burned from.
|
||||||
|
/// @param amount Amount of tokens burned.
|
||||||
|
/// @param sender Address of the account that initiated the crosschain transfer.
|
||||||
|
event CrosschainBurn(address indexed from, uint256 amount, address indexed sender);
|
||||||
|
|
||||||
|
/// @notice Mint tokens through a crosschain transfer.
|
||||||
|
/// @param _to Address to mint tokens to.
|
||||||
|
/// @param _amount Amount of tokens to mint.
|
||||||
|
function crosschainMint(address _to, uint256 _amount) external;
|
||||||
|
|
||||||
|
/// @notice Burn tokens through a crosschain transfer.
|
||||||
|
/// @param _from Address to burn tokens from.
|
||||||
|
/// @param _amount Amount of tokens to burn.
|
||||||
|
function crosschainBurn(address _from, uint256 _amount) external;
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
/// @title Predeploys
|
||||||
|
/// @notice Contains constant addresses for protocol contracts that are pre-deployed to the L2 system.
|
||||||
|
// This excludes the preinstalls (non-protocol contracts).
|
||||||
|
library Predeploys {
|
||||||
|
/// @notice Number of predeploy-namespace addresses reserved for protocol usage.
|
||||||
|
uint256 internal constant PREDEPLOY_COUNT = 2048;
|
||||||
|
|
||||||
|
/// @custom:legacy
|
||||||
|
/// @notice Address of the LegacyMessagePasser predeploy. Deprecate. Use the updated
|
||||||
|
/// L2ToL1MessagePasser contract instead.
|
||||||
|
address internal constant LEGACY_MESSAGE_PASSER = 0x4200000000000000000000000000000000000000;
|
||||||
|
|
||||||
|
/// @custom:legacy
|
||||||
|
/// @notice Address of the L1MessageSender predeploy. Deprecated. Use L2CrossDomainMessenger
|
||||||
|
/// or access tx.origin (or msg.sender) in a L1 to L2 transaction instead.
|
||||||
|
/// Not embedded into new OP-Stack chains.
|
||||||
|
address internal constant L1_MESSAGE_SENDER = 0x4200000000000000000000000000000000000001;
|
||||||
|
|
||||||
|
/// @custom:legacy
|
||||||
|
/// @notice Address of the DeployerWhitelist predeploy. No longer active.
|
||||||
|
address internal constant DEPLOYER_WHITELIST = 0x4200000000000000000000000000000000000002;
|
||||||
|
|
||||||
|
/// @notice Address of the canonical WETH contract.
|
||||||
|
address internal constant WETH = 0x4200000000000000000000000000000000000006;
|
||||||
|
|
||||||
|
/// @notice Address of the L2CrossDomainMessenger predeploy.
|
||||||
|
address internal constant L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000007;
|
||||||
|
|
||||||
|
/// @notice Address of the GasPriceOracle predeploy. Includes fee information
|
||||||
|
/// and helpers for computing the L1 portion of the transaction fee.
|
||||||
|
address internal constant GAS_PRICE_ORACLE = 0x420000000000000000000000000000000000000F;
|
||||||
|
|
||||||
|
/// @notice Address of the L2StandardBridge predeploy.
|
||||||
|
address internal constant L2_STANDARD_BRIDGE = 0x4200000000000000000000000000000000000010;
|
||||||
|
|
||||||
|
//// @notice Address of the SequencerFeeWallet predeploy.
|
||||||
|
address internal constant SEQUENCER_FEE_WALLET = 0x4200000000000000000000000000000000000011;
|
||||||
|
|
||||||
|
/// @notice Address of the OptimismMintableERC20Factory predeploy.
|
||||||
|
address internal constant OPTIMISM_MINTABLE_ERC20_FACTORY = 0x4200000000000000000000000000000000000012;
|
||||||
|
|
||||||
|
/// @custom:legacy
|
||||||
|
/// @notice Address of the L1BlockNumber predeploy. Deprecated. Use the L1Block predeploy
|
||||||
|
/// instead, which exposes more information about the L1 state.
|
||||||
|
address internal constant L1_BLOCK_NUMBER = 0x4200000000000000000000000000000000000013;
|
||||||
|
|
||||||
|
/// @notice Address of the L2ERC721Bridge predeploy.
|
||||||
|
address internal constant L2_ERC721_BRIDGE = 0x4200000000000000000000000000000000000014;
|
||||||
|
|
||||||
|
/// @notice Address of the L1Block predeploy.
|
||||||
|
address internal constant L1_BLOCK_ATTRIBUTES = 0x4200000000000000000000000000000000000015;
|
||||||
|
|
||||||
|
/// @notice Address of the L2ToL1MessagePasser predeploy.
|
||||||
|
address internal constant L2_TO_L1_MESSAGE_PASSER = 0x4200000000000000000000000000000000000016;
|
||||||
|
|
||||||
|
/// @notice Address of the OptimismMintableERC721Factory predeploy.
|
||||||
|
address internal constant OPTIMISM_MINTABLE_ERC721_FACTORY = 0x4200000000000000000000000000000000000017;
|
||||||
|
|
||||||
|
/// @notice Address of the ProxyAdmin predeploy.
|
||||||
|
address internal constant PROXY_ADMIN = 0x4200000000000000000000000000000000000018;
|
||||||
|
|
||||||
|
/// @notice Address of the BaseFeeVault predeploy.
|
||||||
|
address internal constant BASE_FEE_VAULT = 0x4200000000000000000000000000000000000019;
|
||||||
|
|
||||||
|
/// @notice Address of the L1FeeVault predeploy.
|
||||||
|
address internal constant L1_FEE_VAULT = 0x420000000000000000000000000000000000001A;
|
||||||
|
|
||||||
|
/// @notice Address of the SchemaRegistry predeploy.
|
||||||
|
address internal constant SCHEMA_REGISTRY = 0x4200000000000000000000000000000000000020;
|
||||||
|
|
||||||
|
/// @notice Address of the EAS predeploy.
|
||||||
|
address internal constant EAS = 0x4200000000000000000000000000000000000021;
|
||||||
|
|
||||||
|
/// @notice Address of the GovernanceToken predeploy.
|
||||||
|
address internal constant GOVERNANCE_TOKEN = 0x4200000000000000000000000000000000000042;
|
||||||
|
|
||||||
|
/// @custom:legacy
|
||||||
|
/// @notice Address of the LegacyERC20ETH predeploy. Deprecated. Balances are migrated to the
|
||||||
|
/// state trie as of the Bedrock upgrade. Contract has been locked and write functions
|
||||||
|
/// can no longer be accessed.
|
||||||
|
address internal constant LEGACY_ERC20_ETH = 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000;
|
||||||
|
|
||||||
|
/// @notice Address of the CrossL2Inbox predeploy.
|
||||||
|
address internal constant CROSS_L2_INBOX = 0x4200000000000000000000000000000000000022;
|
||||||
|
|
||||||
|
/// @notice Address of the L2ToL2CrossDomainMessenger predeploy.
|
||||||
|
address internal constant L2_TO_L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000023;
|
||||||
|
|
||||||
|
/// @notice Address of the SuperchainWETH predeploy.
|
||||||
|
address internal constant SUPERCHAIN_WETH = 0x4200000000000000000000000000000000000024;
|
||||||
|
|
||||||
|
/// @notice Address of the ETHLiquidity predeploy.
|
||||||
|
address internal constant ETH_LIQUIDITY = 0x4200000000000000000000000000000000000025;
|
||||||
|
|
||||||
|
/// @notice Address of the OptimismSuperchainERC20Factory predeploy.
|
||||||
|
address internal constant OPTIMISM_SUPERCHAIN_ERC20_FACTORY = 0x4200000000000000000000000000000000000026;
|
||||||
|
|
||||||
|
/// @notice Address of the OptimismSuperchainERC20Beacon predeploy.
|
||||||
|
address internal constant OPTIMISM_SUPERCHAIN_ERC20_BEACON = 0x4200000000000000000000000000000000000027;
|
||||||
|
|
||||||
|
// TODO: Precalculate the address of the implementation contract
|
||||||
|
/// @notice Arbitrary address of the OptimismSuperchainERC20 implementation contract.
|
||||||
|
address internal constant OPTIMISM_SUPERCHAIN_ERC20 = 0xB9415c6cA93bdC545D4c5177512FCC22EFa38F28;
|
||||||
|
|
||||||
|
/// @notice Address of the SuperchainTokenBridge predeploy.
|
||||||
|
address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;
|
||||||
|
|
||||||
|
/// @notice Returns the name of the predeploy at the given address.
|
||||||
|
function getName(address _addr) internal pure returns (string memory out_) {
|
||||||
|
require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy");
|
||||||
|
if (_addr == LEGACY_MESSAGE_PASSER) return "LegacyMessagePasser";
|
||||||
|
if (_addr == L1_MESSAGE_SENDER) return "L1MessageSender";
|
||||||
|
if (_addr == DEPLOYER_WHITELIST) return "DeployerWhitelist";
|
||||||
|
if (_addr == WETH) return "WETH";
|
||||||
|
if (_addr == L2_CROSS_DOMAIN_MESSENGER) return "L2CrossDomainMessenger";
|
||||||
|
if (_addr == GAS_PRICE_ORACLE) return "GasPriceOracle";
|
||||||
|
if (_addr == L2_STANDARD_BRIDGE) return "L2StandardBridge";
|
||||||
|
if (_addr == SEQUENCER_FEE_WALLET) return "SequencerFeeVault";
|
||||||
|
if (_addr == OPTIMISM_MINTABLE_ERC20_FACTORY) return "OptimismMintableERC20Factory";
|
||||||
|
if (_addr == L1_BLOCK_NUMBER) return "L1BlockNumber";
|
||||||
|
if (_addr == L2_ERC721_BRIDGE) return "L2ERC721Bridge";
|
||||||
|
if (_addr == L1_BLOCK_ATTRIBUTES) return "L1Block";
|
||||||
|
if (_addr == L2_TO_L1_MESSAGE_PASSER) return "L2ToL1MessagePasser";
|
||||||
|
if (_addr == OPTIMISM_MINTABLE_ERC721_FACTORY) return "OptimismMintableERC721Factory";
|
||||||
|
if (_addr == PROXY_ADMIN) return "ProxyAdmin";
|
||||||
|
if (_addr == BASE_FEE_VAULT) return "BaseFeeVault";
|
||||||
|
if (_addr == L1_FEE_VAULT) return "L1FeeVault";
|
||||||
|
if (_addr == SCHEMA_REGISTRY) return "SchemaRegistry";
|
||||||
|
if (_addr == EAS) return "EAS";
|
||||||
|
if (_addr == GOVERNANCE_TOKEN) return "GovernanceToken";
|
||||||
|
if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH";
|
||||||
|
if (_addr == CROSS_L2_INBOX) return "CrossL2Inbox";
|
||||||
|
if (_addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) return "L2ToL2CrossDomainMessenger";
|
||||||
|
if (_addr == SUPERCHAIN_WETH) return "SuperchainWETH";
|
||||||
|
if (_addr == ETH_LIQUIDITY) return "ETHLiquidity";
|
||||||
|
if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory";
|
||||||
|
if (_addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) return "OptimismSuperchainERC20Beacon";
|
||||||
|
if (_addr == SUPERCHAIN_TOKEN_BRIDGE) return "SuperchainTokenBridge";
|
||||||
|
revert("Predeploys: unnamed predeploy");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Returns true if the predeploy is not proxied.
|
||||||
|
function notProxied(address _addr) internal pure returns (bool) {
|
||||||
|
return _addr == GOVERNANCE_TOKEN || _addr == WETH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Returns true if the address is a defined predeploy that is embedded into new OP-Stack chains.
|
||||||
|
function isSupportedPredeploy(address _addr, bool _useInterop) internal pure returns (bool) {
|
||||||
|
return _addr == LEGACY_MESSAGE_PASSER || _addr == DEPLOYER_WHITELIST || _addr == WETH
|
||||||
|
|| _addr == L2_CROSS_DOMAIN_MESSENGER || _addr == GAS_PRICE_ORACLE || _addr == L2_STANDARD_BRIDGE
|
||||||
|
|| _addr == SEQUENCER_FEE_WALLET || _addr == OPTIMISM_MINTABLE_ERC20_FACTORY || _addr == L1_BLOCK_NUMBER
|
||||||
|
|| _addr == L2_ERC721_BRIDGE || _addr == L1_BLOCK_ATTRIBUTES || _addr == L2_TO_L1_MESSAGE_PASSER
|
||||||
|
|| _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT
|
||||||
|
|| _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN
|
||||||
|
|| (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER)
|
||||||
|
|| (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY)
|
||||||
|
|| (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY)
|
||||||
|
|| (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON)
|
||||||
|
|| (_useInterop && _addr == SUPERCHAIN_TOKEN_BRIDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPredeployNamespace(address _addr) internal pure returns (bool) {
|
||||||
|
return uint160(_addr) >> 11 == uint160(0x4200000000000000000000000000000000000000) >> 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Function to compute the expected address of the predeploy implementation
|
||||||
|
/// in the genesis state.
|
||||||
|
function predeployToCodeNamespace(address _addr) internal pure returns (address) {
|
||||||
|
require(
|
||||||
|
isPredeployNamespace(_addr), "Predeploys: can only derive code-namespace address for predeploy addresses"
|
||||||
|
);
|
||||||
|
return address(
|
||||||
|
uint160(uint256(uint160(_addr)) & 0xffff | uint256(uint160(0xc0D3C0d3C0d3C0D3c0d3C0d3c0D3C0d3c0d30000)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.28;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/utils/Strings.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title TransitionMintLib
|
||||||
|
* @notice Library for handling transition minting logic
|
||||||
|
*/
|
||||||
|
library TransitionMintLib {
|
||||||
|
function getEthSignedMessageHash(bytes32 _messageHash) internal pure returns (bytes32) {
|
||||||
|
/*
|
||||||
|
Signature is produced by signing a keccak256 hash with the following format:
|
||||||
|
"\x19Ethereum Signed Message\n" + len(msg) + msg
|
||||||
|
*/
|
||||||
|
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString(address x) internal pure returns (string memory) {
|
||||||
|
bytes memory s = new bytes(40);
|
||||||
|
for (uint i = 0; i < 20; i++) {
|
||||||
|
bytes1 b = bytes1(uint8(uint(uint160(x)) / (2 ** (8 * (19 - i)))));
|
||||||
|
bytes1 hi = bytes1(uint8(b) / 16);
|
||||||
|
bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi));
|
||||||
|
s[2 * i] = char(hi);
|
||||||
|
s[2 * i + 1] = char(lo);
|
||||||
|
}
|
||||||
|
return string(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function char(bytes1 b) internal pure returns (bytes1 c) {
|
||||||
|
if (uint8(b) < 10) return bytes1(uint8(b) + 0x30);
|
||||||
|
else return bytes1(uint8(b) + 0x57);
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageHash(
|
||||||
|
address holder,
|
||||||
|
uint256 snapshotQuantity
|
||||||
|
) internal pure returns (bytes32) {
|
||||||
|
return keccak256(abi.encodePacked(
|
||||||
|
"10grans migration address: ",
|
||||||
|
toString(holder),
|
||||||
|
" wei: ",
|
||||||
|
Strings.toString(snapshotQuantity)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) internal pure returns (address) {
|
||||||
|
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
|
||||||
|
return ecrecover(_ethSignedMessageHash, v, r, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
|
||||||
|
require(sig.length == 65, "invalid signature length");
|
||||||
|
|
||||||
|
assembly {
|
||||||
|
/*
|
||||||
|
First 32 bytes stores the length of the signature
|
||||||
|
|
||||||
|
add(sig, 32) = pointer of sig + 32
|
||||||
|
effectively, skips first 32 bytes of signature
|
||||||
|
|
||||||
|
mload(p) loads next 32 bytes starting at the memory address p into memory
|
||||||
|
*/
|
||||||
|
|
||||||
|
// first 32 bytes, after the length prefix
|
||||||
|
r := mload(add(sig, 32))
|
||||||
|
// second 32 bytes
|
||||||
|
s := mload(add(sig, 64))
|
||||||
|
// final byte (first byte of the next 32 bytes)
|
||||||
|
v := byte(0, mload(add(sig, 96)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyMintSignature(
|
||||||
|
address holder,
|
||||||
|
uint256 snapshotQuantity,
|
||||||
|
bytes memory signature,
|
||||||
|
address signer
|
||||||
|
) internal pure returns (bool) {
|
||||||
|
bytes32 mh = messageHash(holder, snapshotQuantity);
|
||||||
|
bytes32 ethSignedMessageHash = getEthSignedMessageHash(mh);
|
||||||
|
return recoverSigner(ethSignedMessageHash, signature) == signer;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
# rename to .env.{environment}
|
||||||
|
TOKEN_NAME=MrTestToken
|
||||||
|
TOKEN_SYMBOL=TEST
|
||||||
|
TOKEN_AMOUNT=1000
|
||||||
|
BRIDGE_ADDRESS=0x4200000000000000000000000000000000000010
|
||||||
|
MNEMONIC="special person entering our world egg yolks"
|
||||||
|
INFURA_KEY=bananaphone
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { config as dotenvConfig } from "dotenv";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import type { HardhatUserConfig } from "hardhat/config";
|
||||||
|
import "@nomicfoundation/hardhat-toolbox-viem";
|
||||||
|
import { env } from "./lib/common";
|
||||||
|
|
||||||
|
[
|
||||||
|
`.env.${process.env.APP_ENV}.contracts`,
|
||||||
|
`.env.${process.env.APP_ENV}.data`,
|
||||||
|
`.env.${process.env.APP_ENV}`
|
||||||
|
]
|
||||||
|
.forEach((dotenvConfigPath) => {
|
||||||
|
const path = resolve(__dirname, dotenvConfigPath);
|
||||||
|
dotenvConfig({ path, override: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
const TEST_MNEMONIC = "test test test test test test test test test test test junk";
|
||||||
|
|
||||||
|
const config: HardhatUserConfig = {
|
||||||
|
solidity: "0.8.28",
|
||||||
|
networks: {
|
||||||
|
localhost: {
|
||||||
|
url: "http://127.0.0.1:8545",
|
||||||
|
accounts: {
|
||||||
|
mnemonic: process.env.MNEMONIC || TEST_MNEMONIC
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mainnet: {
|
||||||
|
url: env("MAINNET_RPC_URL"),
|
||||||
|
accounts: {
|
||||||
|
mnemonic: env("MNEMONIC", TEST_MNEMONIC)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
testnet: {
|
||||||
|
url: env("TESTNET_RPC_URL"),
|
||||||
|
accounts: {
|
||||||
|
mnemonic: env("MNEMONIC", TEST_MNEMONIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
etherscan: {
|
||||||
|
apiKey: process.env.ETHERSCAN_KEY
|
||||||
|
},
|
||||||
|
sourcify: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
defaultNetwork: "localhost"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
|
@ -0,0 +1,21 @@
|
||||||
|
// This setup uses Hardhat Ignition to manage smart contract deployments.
|
||||||
|
// Learn more about it at https://hardhat.org/ignition
|
||||||
|
|
||||||
|
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
||||||
|
import { parseEther } from "viem";
|
||||||
|
|
||||||
|
const JAN_1ST_2030 = 1893456000;
|
||||||
|
const ONE_GWEI: bigint = parseEther("0.001");
|
||||||
|
|
||||||
|
const LockModule = buildModule("LockModule", (m) => {
|
||||||
|
const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030);
|
||||||
|
const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI);
|
||||||
|
|
||||||
|
const lock = m.contract("Lock", [unlockTime], {
|
||||||
|
value: lockedAmount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { lock };
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LockModule;
|
|
@ -0,0 +1,64 @@
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export const env = (variable: string, defaultValue?: string): string => {
|
||||||
|
if (process.env[variable]) return process.env[variable] as string;
|
||||||
|
else if (defaultValue) return defaultValue;
|
||||||
|
else throw new Error(`Environment variable: "${variable}" not set`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export class DataRecorder {
|
||||||
|
public readonly filename;
|
||||||
|
private data: Record<string, string> = {};
|
||||||
|
|
||||||
|
constructor(suffix: string) {
|
||||||
|
if (!process.env.APP_ENV) throw new Error("Environment variable APP_ENV must be set");
|
||||||
|
this.filename = `./.env.${process.env.APP_ENV}.${suffix}`;
|
||||||
|
this.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
set = (k: string, v: string | number | bigint | boolean) => {
|
||||||
|
if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") v = v.toString();
|
||||||
|
this.data[k] = v;
|
||||||
|
this.write();
|
||||||
|
};
|
||||||
|
|
||||||
|
write = () => {
|
||||||
|
fs.writeFileSync(this.filename, this.toFileFormat());
|
||||||
|
};
|
||||||
|
|
||||||
|
toFileFormat = () => Object.entries(this.data).reduce((str, [key, value]) => {
|
||||||
|
const qte = value.includes(" ") ? '"' : "";
|
||||||
|
return str + `${key}=${qte}${JSON.stringify(value).slice(1, -1)}${qte}\n`;
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
toString = () => Object.entries(this.data).reduce((str, [key, value]) => str + `${key} = \`${value}\`\n`, "");
|
||||||
|
|
||||||
|
private read = () => {
|
||||||
|
const RE = /^(?<key>[a-z0-9]+)="?(?<value>.*?)"?$/mi;
|
||||||
|
|
||||||
|
let errs = "";
|
||||||
|
let count = 0;
|
||||||
|
try {
|
||||||
|
const lines = fs.readFileSync(this.filename, "utf8").split("\n").filter((line) => line.trim() !== "");
|
||||||
|
lines.forEach((line, i) => {
|
||||||
|
const match = RE.exec(line)?.groups;
|
||||||
|
if (match) {
|
||||||
|
const { key, value } = match;
|
||||||
|
this.data[key] = JSON.parse(`"${value}"`);
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
else if (line.startsWith("#")) { /* comment */ }
|
||||||
|
else {
|
||||||
|
errs += `Line ${i} is bad: ${line}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.debug(`Read ${count} lines from ${this.filename}`);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// TODO make it ignore file not found specifically.
|
||||||
|
}
|
||||||
|
if (errs !== "") throw new Error(`DataRecorder: ${this.filename} is bad:\n` + errs);
|
||||||
|
};
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "base-10grans",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "10Grans on Base blockchain.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"solidity"
|
||||||
|
],
|
||||||
|
"author": "moon.eth",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@nomicfoundation/hardhat-toolbox-viem": "^3.0.0",
|
||||||
|
"@openzeppelin/contracts": "^5.2.0",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"hardhat": "^2.22.18"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
import {
|
||||||
|
time,
|
||||||
|
loadFixture,
|
||||||
|
} from "@nomicfoundation/hardhat-toolbox-viem/network-helpers";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import hre from "hardhat";
|
||||||
|
import { getAddress, parseGwei } from "viem";
|
||||||
|
|
||||||
|
describe("Lock", function () {
|
||||||
|
// We define a fixture to reuse the same setup in every test.
|
||||||
|
// We use loadFixture to run this setup once, snapshot that state,
|
||||||
|
// and reset Hardhat Network to that snapshot in every test.
|
||||||
|
async function deployOneYearLockFixture() {
|
||||||
|
const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
|
||||||
|
|
||||||
|
const lockedAmount = parseGwei("1");
|
||||||
|
const unlockTime = BigInt((await time.latest()) + ONE_YEAR_IN_SECS);
|
||||||
|
|
||||||
|
// Contracts are deployed using the first signer/account by default
|
||||||
|
const [owner, otherAccount] = await hre.viem.getWalletClients();
|
||||||
|
|
||||||
|
const lock = await hre.viem.deployContract("Lock", [unlockTime], {
|
||||||
|
value: lockedAmount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const publicClient = await hre.viem.getPublicClient();
|
||||||
|
|
||||||
|
return {
|
||||||
|
lock,
|
||||||
|
unlockTime,
|
||||||
|
lockedAmount,
|
||||||
|
owner,
|
||||||
|
otherAccount,
|
||||||
|
publicClient,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Deployment", function () {
|
||||||
|
it("Should set the right unlockTime", async function () {
|
||||||
|
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);
|
||||||
|
|
||||||
|
expect(await lock.read.unlockTime()).to.equal(unlockTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should set the right owner", async function () {
|
||||||
|
const { lock, owner } = await loadFixture(deployOneYearLockFixture);
|
||||||
|
|
||||||
|
expect(await lock.read.owner()).to.equal(
|
||||||
|
getAddress(owner.account.address)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should receive and store the funds to lock", async function () {
|
||||||
|
const { lock, lockedAmount, publicClient } = await loadFixture(
|
||||||
|
deployOneYearLockFixture
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await publicClient.getBalance({
|
||||||
|
address: lock.address,
|
||||||
|
})
|
||||||
|
).to.equal(lockedAmount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should fail if the unlockTime is not in the future", async function () {
|
||||||
|
// We don't use the fixture here because we want a different deployment
|
||||||
|
const latestTime = BigInt(await time.latest());
|
||||||
|
await expect(
|
||||||
|
hre.viem.deployContract("Lock", [latestTime], {
|
||||||
|
value: 1n,
|
||||||
|
})
|
||||||
|
).to.be.rejectedWith("Unlock time should be in the future");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Withdrawals", function () {
|
||||||
|
describe("Validations", function () {
|
||||||
|
it("Should revert with the right error if called too soon", async function () {
|
||||||
|
const { lock } = await loadFixture(deployOneYearLockFixture);
|
||||||
|
|
||||||
|
await expect(lock.write.withdraw()).to.be.rejectedWith(
|
||||||
|
"You can't withdraw yet"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should revert with the right error if called from another account", async function () {
|
||||||
|
const { lock, unlockTime, otherAccount } = await loadFixture(
|
||||||
|
deployOneYearLockFixture
|
||||||
|
);
|
||||||
|
|
||||||
|
// We can increase the time in Hardhat Network
|
||||||
|
await time.increaseTo(unlockTime);
|
||||||
|
|
||||||
|
// We retrieve the contract with a different account to send a transaction
|
||||||
|
const lockAsOtherAccount = await hre.viem.getContractAt(
|
||||||
|
"Lock",
|
||||||
|
lock.address,
|
||||||
|
{ client: { wallet: otherAccount } }
|
||||||
|
);
|
||||||
|
await expect(lockAsOtherAccount.write.withdraw()).to.be.rejectedWith(
|
||||||
|
"You aren't the owner"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
|
||||||
|
const { lock, unlockTime } = await loadFixture(
|
||||||
|
deployOneYearLockFixture
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transactions are sent using the first signer by default
|
||||||
|
await time.increaseTo(unlockTime);
|
||||||
|
|
||||||
|
await expect(lock.write.withdraw()).to.be.fulfilled;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Events", function () {
|
||||||
|
it("Should emit an event on withdrawals", async function () {
|
||||||
|
const { lock, unlockTime, lockedAmount, publicClient } =
|
||||||
|
await loadFixture(deployOneYearLockFixture);
|
||||||
|
|
||||||
|
await time.increaseTo(unlockTime);
|
||||||
|
|
||||||
|
const hash = await lock.write.withdraw();
|
||||||
|
await publicClient.waitForTransactionReceipt({ hash });
|
||||||
|
|
||||||
|
// get the withdrawal events in the latest block
|
||||||
|
const withdrawalEvents = await lock.getEvents.Withdrawal();
|
||||||
|
expect(withdrawalEvents).to.have.lengthOf(1);
|
||||||
|
expect(withdrawalEvents[0].args.amount).to.equal(lockedAmount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue