Compare commits

..

No commits in common. "main" and "arbitrum" have entirely different histories.

16 changed files with 1800 additions and 513 deletions

4
.gitignore vendored
View File

@ -1,14 +1,10 @@
node_modules
.env
.env.*
coverage
coverage.json
typechain
typechain-types
.l1-token-address.*
.l2-token-address.*
# Hardhat files
cache
artifacts

View File

@ -1,25 +1,11 @@
# Optimism-Compatible Token
# 10grans-NG
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.
10grans on Arbitrum chain, with a bridge to Ethereum.
```shell
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
npx hardhat help
npx hardhat test
REPORT_GAS=true npx hardhat test
npx hardhat node
npx hardhat run scripts/deploy.ts
```
# Depositing to L2
Use this tool: https://git.shipoclu.com/moon/l2-base-bridging
# License
MIT license.

View File

@ -1,11 +0,0 @@
// 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);
}
}

View File

@ -1,116 +0,0 @@
// 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;
}
}

View File

@ -1,11 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
pragma solidity ^0.8.9;
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";
@ -14,10 +13,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 AbstractToken is IERC1363, ERC20, ERC20Burnable, Pausable, Ownable, ERC20Permit, ERC20Votes, ERC20FlashMint {
abstract contract AbstractGrans is IERC1363, ERC20, ERC20Burnable, Pausable, Ownable, ERC20Permit, ERC20Votes, ERC20FlashMint {
using Address for address;
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC20Permit(_name) {}
constructor() ERC20("10Grans", "GRANS") ERC20Permit("10Grans") {}
function pause() public onlyOwner {
_pause();

View File

@ -0,0 +1,30 @@
// 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);
}
}

View File

@ -0,0 +1,85 @@
// 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;
}
}

View File

@ -1,7 +0,0 @@
# 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

View File

@ -1,67 +1,17 @@
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", // don"t make this higher
version: "0.8.19",
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;

1845
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "10grans-ng",
"version": "1.0.0",
"description": "10grans on Base",
"description": "10grans on Arbitrum",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
@ -14,8 +14,7 @@
"cryptocurrency",
"erc-20",
"token",
"solidity",
"coinbase"
"solidity"
],
"author": "moon@shipoclu.com",
"license": "MIT",
@ -27,10 +26,8 @@
"prettier-plugin-solidity": "^1.1.3"
},
"dependencies": {
"@eth-optimism/contracts-bedrock": "^0.16.0",
"@arbitrum/token-bridge-contracts": "^1.0.0-beta.0",
"@openzeppelin/contracts": "^4.9.3",
"dotenv": "^16.3.1",
"erc-payable-token": "^4.9.3",
"ethers": "^6.7.1"
"erc-payable-token": "^4.9.3"
}
}

View File

@ -1,16 +0,0 @@
#!/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

View File

@ -1,34 +0,0 @@
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;
});

View File

@ -1,36 +0,0 @@
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;
});

View File

@ -1,17 +0,0 @@
#!/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

12
scripts/deploy.ts Normal file
View File

@ -0,0 +1,12 @@
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;
});