Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
Moon Man | 2b6f14aa16 | |
Moon Man | 6a99fda774 | |
Moon Man | b9fc687822 | |
Moon Man | e8ab27e7c9 | |
Moon Man | bb7dcc42b8 | |
Moon Man | aff7031a2f | |
Moon Man | 0246e47a58 | |
Moon Man | 68af3cb3f0 | |
Moon Man | 1b8ecb5c37 | |
Moon Man | f19428e409 | |
Moon Man | c952ccc38d | |
Moon Man | c69b64994a | |
Moon Man | 725f30100c | |
Moon Man | a03053efa1 | |
Moon Man | 5f95b7d3ac | |
Moon Man | 2d0702934f | |
Moon Man | fa4ede8fbc | |
Moon Man | 98af8dccad | |
Moon Man | ffae5e74d3 | |
Moon Man | d8bc375d4c | |
Moon Man | 0fab17085f | |
Moon Man | 6e7f451080 | |
Moon Man | 091be423aa |
|
@ -1,10 +1,14 @@
|
|||
node_modules
|
||||
.env
|
||||
.env.*
|
||||
coverage
|
||||
coverage.json
|
||||
typechain
|
||||
typechain-types
|
||||
|
||||
.l1-token-address.*
|
||||
.l2-token-address.*
|
||||
|
||||
# Hardhat files
|
||||
cache
|
||||
artifacts
|
||||
|
|
28
README.md
28
README.md
|
@ -1,11 +1,25 @@
|
|||
# 10grans-NG
|
||||
# Optimism-Compatible Token
|
||||
|
||||
10grans on Arbitrum chain, with a bridge to Ethereum.
|
||||
EVM-compatible ERC-20 token with extra features, capable of being bridged to an
|
||||
Optimism-compatible L2. Built for Base blockchain and the 10Grans token,
|
||||
but general enough to be reused.
|
||||
|
||||
```shell
|
||||
npx hardhat help
|
||||
npx hardhat test
|
||||
REPORT_GAS=true npx hardhat test
|
||||
npx hardhat node
|
||||
npx hardhat run scripts/deploy.ts
|
||||
npx hardhat compile
|
||||
|
||||
cp env.example .env.dev
|
||||
# (customize your env file here first)
|
||||
|
||||
scripts/deploy-dev.sh
|
||||
|
||||
# optionally verify your contract on L2, customize the following:
|
||||
NODE_ENV=dev npx hardhat verify --network base-goerli 0xl2tokenaddress MrTestToken TEST 0xbridgeonl2 0xl1tokenaddress
|
||||
```
|
||||
|
||||
# Depositing to L2
|
||||
|
||||
Use this tool: https://git.shipoclu.com/moon/l2-base-bridging
|
||||
|
||||
# License
|
||||
|
||||
MIT license.
|
|
@ -1,10 +1,11 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.9;
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
// Without paying gas, token holders will be able to allow third parties to transfer from their account. (EIP2612)
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol";
|
||||
|
@ -13,10 +14,10 @@ import "erc-payable-token/contracts/token/ERC1363/IERC1363.sol";
|
|||
import "erc-payable-token/contracts/token/ERC1363/IERC1363Spender.sol";
|
||||
import "erc-payable-token/contracts/token/ERC1363/IERC1363Receiver.sol";
|
||||
|
||||
abstract contract AbstractGrans is IERC1363, ERC20, ERC20Burnable, Pausable, Ownable, ERC20Permit, ERC20Votes, ERC20FlashMint {
|
||||
abstract contract AbstractToken is IERC1363, ERC20, ERC20Burnable, Pausable, Ownable, ERC20Permit, ERC20Votes, ERC20FlashMint {
|
||||
using Address for address;
|
||||
|
||||
constructor() ERC20("10Grans", "GRANS") ERC20Permit("10Grans") {}
|
||||
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC20Permit(_name) {}
|
||||
|
||||
function pause() public onlyOwner {
|
||||
_pause();
|
|
@ -0,0 +1,11 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "./AbstractToken.sol";
|
||||
|
||||
contract L1Token is AbstractToken {
|
||||
|
||||
constructor(string memory _name, string memory _symbol, uint256 _fixedMint) AbstractToken(_name, _symbol) {
|
||||
_mint(msg.sender, _fixedMint * 10 ** 18);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "./AbstractToken.sol";
|
||||
import "@eth-optimism/contracts-bedrock/contracts/universal/IOptimismMintableERC20.sol";
|
||||
|
||||
contract L2Token is AbstractToken, IOptimismMintableERC20 {
|
||||
/// @notice Address of the corresponding version of this token on the remote chain.
|
||||
address public immutable REMOTE_TOKEN;
|
||||
|
||||
/// @notice Address of the StandardBridge on this network.
|
||||
address public immutable BRIDGE;
|
||||
|
||||
/// @notice Emitted whenever tokens are minted for an account.
|
||||
/// @param account Address of the account tokens are being minted for.
|
||||
/// @param amount Amount of tokens minted.
|
||||
event Mint(address indexed account, uint256 amount);
|
||||
|
||||
/// @notice Emitted whenever tokens are burned from an account.
|
||||
/// @param account Address of the account tokens are being burned from.
|
||||
/// @param amount Amount of tokens burned.
|
||||
event Burn(address indexed account, uint256 amount);
|
||||
|
||||
/// @notice A modifier that only allows the bridge to call
|
||||
modifier onlyBridge() {
|
||||
require(_msgSender() == BRIDGE, "OptimismMintableERC20: only bridge can mint and burn");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(string memory _name, string memory _symbol, address _bridge, address _remoteToken) AbstractToken(_name, _symbol) {
|
||||
BRIDGE = _bridge;
|
||||
REMOTE_TOKEN = _remoteToken;
|
||||
}
|
||||
|
||||
function balanceOf(address account) public view virtual override(AbstractToken) returns (uint256) {
|
||||
return AbstractToken.balanceOf(account);
|
||||
}
|
||||
|
||||
function transferFrom(address sender, address recipient, uint256 amount) public virtual override(AbstractToken) returns (bool) {
|
||||
return AbstractToken.transferFrom(sender, recipient, amount);
|
||||
}
|
||||
|
||||
/// @notice Allows the StandardBridge on this network to mint tokens.
|
||||
/// @param _to Address to mint tokens to.
|
||||
/// @param _amount Amount of tokens to mint.
|
||||
// ONLY bridge is allowed to mint, for security reasons.
|
||||
function mint(
|
||||
address _to,
|
||||
uint256 _amount
|
||||
)
|
||||
external
|
||||
virtual
|
||||
override(IOptimismMintableERC20)
|
||||
onlyBridge
|
||||
{
|
||||
_mint(_to, _amount);
|
||||
emit Mint(_to, _amount);
|
||||
}
|
||||
|
||||
/// @notice Allows the StandardBridge on this network to burn tokens.
|
||||
/// @param _from Address to burn tokens from.
|
||||
/// @param _amount Amount of tokens to burn.
|
||||
// ONLY bridge is allowed to burn, for security reasons.
|
||||
function burn(
|
||||
address _from,
|
||||
uint256 _amount
|
||||
)
|
||||
external
|
||||
virtual
|
||||
override(IOptimismMintableERC20)
|
||||
onlyBridge
|
||||
{
|
||||
_burn(_from, _amount);
|
||||
emit Burn(_from, _amount);
|
||||
}
|
||||
|
||||
/// @notice ERC165 interface check function.
|
||||
/// @param _interfaceId Interface ID to check.
|
||||
/// @return Whether or not the interface is supported by this contract.
|
||||
function supportsInterface(bytes4 _interfaceId) public pure virtual override(AbstractToken, IERC165) returns (bool) {
|
||||
bytes4 iface1 = type(IERC165).interfaceId;
|
||||
// Interface corresponding to the legacy L2StandardERC20.
|
||||
bytes4 iface2 = type(ILegacyMintableERC20).interfaceId;
|
||||
// Interface corresponding to the updated OptimismMintableERC20 (this contract).
|
||||
bytes4 iface3 = type(IOptimismMintableERC20).interfaceId;
|
||||
return _interfaceId == iface1 || _interfaceId == iface2 || _interfaceId == iface3;
|
||||
}
|
||||
|
||||
/// @custom:legacy
|
||||
/// @notice Legacy getter for REMOTE_TOKEN.
|
||||
// Is this needed?
|
||||
function remoteToken() public view virtual override(IOptimismMintableERC20) returns (address) {
|
||||
return REMOTE_TOKEN;
|
||||
}
|
||||
|
||||
/// @custom:legacy
|
||||
/// @notice Legacy getter for BRIDGE.
|
||||
// Is this needed?
|
||||
function bridge() public view virtual override(IOptimismMintableERC20) returns (address) {
|
||||
return BRIDGE;
|
||||
}
|
||||
|
||||
/// @custom:legacy
|
||||
/// @notice Legacy getter for the remote token. Use REMOTE_TOKEN going forward.
|
||||
// Turns out Base blockchain needed this.
|
||||
function l1Token() public virtual view returns (address) {
|
||||
return REMOTE_TOKEN;
|
||||
}
|
||||
|
||||
/// @custom:legacy
|
||||
/// @notice Legacy getter for the bridge. Use BRIDGE going forward.
|
||||
// Turns out Base blockchain needed this.
|
||||
function l2Bridge() public virtual view returns (address) {
|
||||
return BRIDGE;
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.9;
|
||||
|
||||
import "./TenGransAbstractToken.sol";
|
||||
import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/arbitrum/IArbToken.sol";
|
||||
|
||||
contract TenGransArbToken is AbstractGrans, IArbToken {
|
||||
uint256 public immutable cap = 15_000 * 10 ** 18;
|
||||
address public immutable l2Gateway;
|
||||
address public immutable override l1Address;
|
||||
|
||||
modifier onlyGateway() {
|
||||
require(msg.sender == l2Gateway, "ONLY_l2GATEWAY");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _l2Gateway, address _l1Address) AbstractGrans() {
|
||||
l2Gateway = _l2Gateway;
|
||||
l1Address = _l1Address;
|
||||
}
|
||||
|
||||
function bridgeMint(address account, uint256 amount) external virtual override onlyGateway {
|
||||
require(amount + totalSupply() <= cap, "CAP_EXCEEDED");
|
||||
_mint(account, amount);
|
||||
}
|
||||
|
||||
function bridgeBurn(address account, uint256 amount) external virtual override onlyGateway {
|
||||
_burn(account, amount);
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.9;
|
||||
|
||||
import "./TenGransAbstractToken.sol";
|
||||
import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/ICustomToken.sol";
|
||||
|
||||
interface IL1CustomGateway {
|
||||
function registerTokenToL2(
|
||||
address _l2Address,
|
||||
uint256 _maxGas,
|
||||
uint256 _gasPriceBid,
|
||||
uint256 _maxSubmissionCost,
|
||||
address _creditBackAddress
|
||||
) external payable returns (uint256);
|
||||
}
|
||||
|
||||
interface IGatewayRouter2 {
|
||||
function setGateway(
|
||||
address _gateway,
|
||||
uint256 _maxGas,
|
||||
uint256 _gasPriceBid,
|
||||
uint256 _maxSubmissionCost,
|
||||
address _creditBackAddress
|
||||
) external payable returns (uint256);
|
||||
}
|
||||
|
||||
contract TenGransEthToken is AbstractGrans, ICustomToken {
|
||||
address public immutable gateway;
|
||||
address public immutable router;
|
||||
bool private shouldRegisterGateway;
|
||||
|
||||
constructor(address _gateway, address _router) AbstractGrans() {
|
||||
gateway = _gateway;
|
||||
router = _router;
|
||||
_mint(msg.sender, 15_000 * 10 ** 18);
|
||||
}
|
||||
|
||||
function balanceOf(address account) public view virtual override(AbstractGrans, ICustomToken) returns (uint256) {
|
||||
return AbstractGrans.balanceOf(account);
|
||||
}
|
||||
|
||||
function transferFrom(address sender, address recipient, uint256 amount) public virtual override(AbstractGrans, ICustomToken) returns (bool) {
|
||||
return AbstractGrans.transferFrom(sender, recipient, amount);
|
||||
}
|
||||
|
||||
/// @dev we only set shouldRegisterGateway to true when in `registerTokenOnL2`
|
||||
function isArbitrumEnabled() external view override returns (uint8) {
|
||||
require(shouldRegisterGateway, "NOT_EXPECTED_CALL");
|
||||
return uint8(uint16(uint32(uint64(uint128(0xa4b1)))));
|
||||
}
|
||||
|
||||
function registerTokenOnL2(
|
||||
address l2CustomTokenAddress,
|
||||
uint256 maxSubmissionCostForCustomGateway,
|
||||
uint256 maxSubmissionCostForRouter,
|
||||
uint256 maxGasForCustomGateway,
|
||||
uint256 maxGasForRouter,
|
||||
uint256 gasPriceBid,
|
||||
uint256 valueForGateway,
|
||||
uint256 valueForRouter,
|
||||
address creditBackAddress
|
||||
) public payable override onlyOwner {
|
||||
// we temporarily set `shouldRegisterGateway` to true for the callback in registerTokenToL2 to succeed
|
||||
bool prev = shouldRegisterGateway;
|
||||
shouldRegisterGateway = true;
|
||||
|
||||
IL1CustomGateway(gateway).registerTokenToL2{value: valueForGateway}(
|
||||
l2CustomTokenAddress,
|
||||
maxGasForCustomGateway,
|
||||
gasPriceBid,
|
||||
maxSubmissionCostForCustomGateway,
|
||||
creditBackAddress
|
||||
);
|
||||
|
||||
IGatewayRouter2(router).setGateway{value: valueForRouter}(
|
||||
gateway,
|
||||
maxGasForRouter,
|
||||
gasPriceBid,
|
||||
maxSubmissionCostForRouter,
|
||||
creditBackAddress
|
||||
);
|
||||
|
||||
shouldRegisterGateway = prev;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -1,17 +1,67 @@
|
|||
import { HardhatUserConfig } from "hardhat/config";
|
||||
import "@nomicfoundation/hardhat-toolbox";
|
||||
import "hardhat-contract-sizer";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
const env = process.env.NODE_ENV || "local";
|
||||
dotenv.config({ path: `.env.${env}` });
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
version: "0.8.19",
|
||||
version: "0.8.19", // don"t make this higher
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200
|
||||
}
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
"base-mainnet": {
|
||||
url: "https://mainnet.base.org",
|
||||
accounts: {
|
||||
mnemonic: process.env.MNEMONIC as string
|
||||
},
|
||||
gasPrice: 1000000000,
|
||||
},
|
||||
"base-goerli": {
|
||||
url: "https://goerli.base.org",
|
||||
accounts: {
|
||||
mnemonic: process.env.MNEMONIC as string
|
||||
},
|
||||
gasPrice: 1000000000,
|
||||
},
|
||||
"base-local": {
|
||||
url: "http://localhost:8545",
|
||||
accounts: {
|
||||
mnemonic: process.env.MNEMONIC as string
|
||||
},
|
||||
gasPrice: 1000000000,
|
||||
},
|
||||
"eth-goerli": {
|
||||
url: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`,
|
||||
accounts: {
|
||||
mnemonic: process.env.MNEMONIC as string
|
||||
},
|
||||
gasPrice: 1000000000,
|
||||
},
|
||||
},
|
||||
defaultNetwork: "base-local",
|
||||
etherscan: {
|
||||
apiKey: {
|
||||
"base-goerli": "PLACEHOLDER_STRING" // yes this is really the string
|
||||
},
|
||||
customChains: [
|
||||
{
|
||||
network: "base-goerli",
|
||||
chainId: 84531,
|
||||
urls: {
|
||||
apiURL: "https://api-goerli.basescan.org/api",
|
||||
browserURL: "https://goerli.basescan.org"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "10grans-ng",
|
||||
"version": "1.0.0",
|
||||
"description": "10grans on Arbitrum",
|
||||
"description": "10grans on Base",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
@ -14,7 +14,8 @@
|
|||
"cryptocurrency",
|
||||
"erc-20",
|
||||
"token",
|
||||
"solidity"
|
||||
"solidity",
|
||||
"coinbase"
|
||||
],
|
||||
"author": "moon@shipoclu.com",
|
||||
"license": "MIT",
|
||||
|
@ -26,8 +27,10 @@
|
|||
"prettier-plugin-solidity": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arbitrum/token-bridge-contracts": "^1.0.0-beta.0",
|
||||
"@eth-optimism/contracts-bedrock": "^0.16.0",
|
||||
"@openzeppelin/contracts": "^4.9.3",
|
||||
"erc-payable-token": "^4.9.3"
|
||||
"dotenv": "^16.3.1",
|
||||
"erc-payable-token": "^4.9.3",
|
||||
"ethers": "^6.7.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/sh
|
||||
|
||||
NODE_ENV=dev
|
||||
export NODE_ENV
|
||||
|
||||
npx hardhat compile
|
||||
|
||||
npx hardhat --network eth-goerli run scripts/deploy-l1.ts
|
||||
|
||||
if [ "$?" -eq 0 ]
|
||||
then
|
||||
npx hardhat --network base-goerli run scripts/deploy-l2.ts
|
||||
else
|
||||
echo "deploy to L1 failed so aborting L2" >&2
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,34 @@
|
|||
import { ethers } from "hardhat";
|
||||
import fs from "fs/promises";
|
||||
|
||||
async function main() {
|
||||
const env = process.env.NODE_ENV || "local";
|
||||
|
||||
if (!process.env.TOKEN_NAME) throw "Token name not defined";
|
||||
if (!process.env.TOKEN_SYMBOL) throw "Token symbol not defined";
|
||||
if (!process.env.TOKEN_AMOUNT) throw "Token amount not defined";
|
||||
|
||||
const tokenAmount = parseInt(process.env.TOKEN_AMOUNT);
|
||||
|
||||
const L1Token = await ethers.deployContract(
|
||||
"L1Token",
|
||||
[
|
||||
process.env.TOKEN_NAME,
|
||||
process.env.TOKEN_SYMBOL,
|
||||
tokenAmount,
|
||||
]
|
||||
);
|
||||
|
||||
await L1Token.waitForDeployment();
|
||||
const deployedAddress = await L1Token.getAddress();
|
||||
|
||||
console.log(`L1 token deployed to: ${deployedAddress}`);
|
||||
await fs.writeFile(`.l1-token-address.${env}`, deployedAddress);
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { ethers } from "hardhat";
|
||||
import fs from "fs/promises";
|
||||
import dotenv from "dotenv";
|
||||
const env = process.env.NODE_ENV || "local";
|
||||
dotenv.config({ path: `.env.${env}` });
|
||||
|
||||
async function main() {
|
||||
if (!process.env.BRIDGE_ADDRESS) throw "Bridge address not defined";
|
||||
if (!process.env.TOKEN_NAME) throw "Token name not defined";
|
||||
if (!process.env.TOKEN_SYMBOL) throw "Token symbol not defined";
|
||||
|
||||
const L1TokenAddress = (await fs.readFile(`.l1-token-address.${env}`, "utf-8")).trim();
|
||||
|
||||
const L2Token = await ethers.deployContract(
|
||||
"L2Token",
|
||||
[
|
||||
process.env.TOKEN_NAME,
|
||||
process.env.TOKEN_SYMBOL,
|
||||
process.env.BRIDGE_ADDRESS,
|
||||
L1TokenAddress,
|
||||
]
|
||||
);
|
||||
|
||||
await L2Token.waitForDeployment();
|
||||
const deployedAddress = await L2Token.getAddress();
|
||||
|
||||
console.log(`L2 token deployed to: ${deployedAddress}`);
|
||||
await fs.writeFile(`.l2-token-address.${env}`, deployedAddress);
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
NODE_ENV=local
|
||||
export NODE_ENV
|
||||
|
||||
nc -z "127.0.0.1" 8545 >/dev/null 2>&1
|
||||
if [ "$?" -ne 0 ]
|
||||
then
|
||||
echo "local node not running" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npx hardhat compile
|
||||
|
||||
# Same network, for testing reasons only
|
||||
npx hardhat --network base-local run scripts/deploy-l1.ts
|
||||
npx hardhat --network base-local run scripts/deploy-l2.ts
|
|
@ -1,12 +0,0 @@
|
|||
import { ethers } from "hardhat";
|
||||
|
||||
async function main() {
|
||||
// Do stuff here.
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
Loading…
Reference in New Issue