first commit

This commit is contained in:
Moon Man 2025-01-16 22:13:50 -05:00
commit 27d859116e
14 changed files with 8417 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -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

21
README.md Normal file
View File

@ -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.

111
contracts/TenGransToken.sol Normal file
View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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)))
);
}
}

View File

@ -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;
}
}

7
env.example Normal file
View File

@ -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

50
hardhat.config.ts Normal file
View File

@ -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;

21
ignition/modules/Lock.ts Normal file
View File

@ -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;

64
lib/common.ts Normal file
View File

@ -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);
};
}

7666
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@ -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"
}
}

134
test/Lock.ts Normal file
View File

@ -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);
});
});
});
});

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
}
}