From 1abe4c6dc494015d94e90a5cc7226c3661e8b4f3 Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Thu, 21 Aug 2025 17:12:58 -0700 Subject: [PATCH] Scaffold project from send-token-upgrade Why: Bootstrap repository structure using the known baseline to accelerate initial setup while keeping consistent tooling. Test plan: - List files; key scaffolding files exist. - bunx hardhat compile succeeds. --- .env.sample | 2 + .envrc | 1 + .gitignore | 32 ++++++++++++++ Tiltfile | 8 ++++ bin/anvil-deploy | 54 ++++++++++++++++++++++++ bunfig.toml | 2 + contracts/ISendLockbox.sol | 9 ++++ contracts/ISendToken.sol | 8 ++++ contracts/SendLockbox.sol | 51 ++++++++++++++++++++++ contracts/SendToken.sol | 32 ++++++++++++++ hardhat.config.ts | 58 +++++++++++++++++++++++++ ignition/modules/SendToken.ts | 34 +++++++++++++++ package.json | 28 +++++++++++++ test/SendToken.ts | 79 +++++++++++++++++++++++++++++++++++ tsconfig.json | 11 +++++ 15 files changed, 409 insertions(+) create mode 100644 .env.sample create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Tiltfile create mode 100755 bin/anvil-deploy create mode 100644 bunfig.toml create mode 100644 contracts/ISendLockbox.sol create mode 100644 contracts/ISendToken.sol create mode 100644 contracts/SendLockbox.sol create mode 100644 contracts/SendToken.sol create mode 100644 hardhat.config.ts create mode 100644 ignition/modules/SendToken.ts create mode 100644 package.json create mode 100644 test/SendToken.ts create mode 100644 tsconfig.json diff --git a/.env.sample b/.env.sample new file mode 100644 index 000000000..03cf96b05 --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +EOA_DEPLOYER=... +ETHERSCAN_API_KEY=... \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..76f03ce1f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +dotenv_if_exists .env diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e08b8df8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +broadcast/ +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +node_modules +.env + +# Hardhat files +/cache + +# 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 diff --git a/Tiltfile b/Tiltfile new file mode 100644 index 000000000..b6796e93a --- /dev/null +++ b/Tiltfile @@ -0,0 +1,8 @@ +local_resource( + "send-token-upgrade-deploy", + cmd = os.path.join(__file__, "..", "bin", "anvil-deploy"), + resource_deps = [ + "anvil:base", + ], +) + diff --git a/bin/anvil-deploy b/bin/anvil-deploy new file mode 100755 index 000000000..3960db23e --- /dev/null +++ b/bin/anvil-deploy @@ -0,0 +1,54 @@ +#!/usr/bin/env bun run +import "zx/globals"; +$.verbose = true; + +/** + * This script is used to deploy the Send token lockbox contract. + */ + +const RPC_URL = "http://localhost:8546"; +const baseSendMVPDeployer = "0x7F314BffCB437b7046F469dE2457f9C4014931e1"; + +const info = (msg: TemplateStringsArray, ..._: any[]) => + console.log(chalk.blue(msg.join(" "))); + +void (async function main() { + info`Enable auto-mining...`; + await $`cast rpc --rpc-url ${RPC_URL} evm_setAutomine true`; + + info`Impersonating the Base Send MVP Deployer...`; + await $`cast rpc --rpc-url ${RPC_URL} \ + anvil_impersonateAccount \ + ${baseSendMVPDeployer}`; + + info`Funding the Base Send MVP Deployer...`; + await $`cast send --rpc-url ${RPC_URL} \ + --unlocked \ + --from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ + ${baseSendMVPDeployer} \ + --value 10ether`; + + // remove previous deployments + const artifactsDir = path.resolve( + __dirname, + "..", + "ignition", + "deployments", + "chain-845337" + ); + info`Removing previous deployments...`; + await $`rm -rf ${artifactsDir}`; + + info`Deploying SendLockbox...`; + await $`echo yes | bunx hardhat ignition deploy --network anvil ./ignition/modules/SendToken.ts`; + + info`Disable auto-mining...`; + await $`cast rpc --rpc-url ${RPC_URL} evm_setAutomine false`; + + info`Re-enable interval mining... ${$.env.ANVIL_BLOCK_TIME ?? "2"}`; + await $`cast rpc --rpc-url ${RPC_URL} evm_setIntervalMining ${ + $.env.ANVIL_BLOCK_TIME ?? "2" + }`; // mimics Tiltfile default + + console.log(chalk.green("Done!")); +})(); diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 000000000..7185d6993 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[install.scopes] +"0xsend" = { token = "$npm_token", url = "https://registry.npmjs.org/" } diff --git a/contracts/ISendLockbox.sol b/contracts/ISendLockbox.sol new file mode 100644 index 000000000..bd403488c --- /dev/null +++ b/contracts/ISendLockbox.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.28; + +interface ISendLockbox { + event Deposit(address indexed to, uint256 amount); + + function deposit(uint256 amount) external; + function depositTo(address to, uint256 amount) external; +} diff --git a/contracts/ISendToken.sol b/contracts/ISendToken.sol new file mode 100644 index 000000000..2b98c0a22 --- /dev/null +++ b/contracts/ISendToken.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +interface ISendToken is IERC20 { + function mint(address to, uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/SendLockbox.sol b/contracts/SendLockbox.sol new file mode 100644 index 000000000..c137324a4 --- /dev/null +++ b/contracts/SendLockbox.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +import "./ISendToken.sol"; +import "./ISendLockbox.sol"; + +contract SendLockbox is ISendLockbox { + using SafeERC20 for IERC20; + + /// @notice The old ERC20 token of this contract + IERC20 public immutable SEND_V0; + + /// @notice The new ERC20 token of this contract + ISendToken public immutable SEND_V1; + + /// @param sendv0 The address of the old ERC20 contract + /// @param sendv1 The address of the new ERC20 contract + constructor(address sendv0, address sendv1) { + SEND_V0 = IERC20(sendv0); + SEND_V1 = ISendToken(sendv1); + } + + /// @notice Deposit tokens into the lockbox and mints the new token to sender + /// @param amount The amount of tokens to deposit + function deposit(uint256 amount) external { + _deposit(msg.sender, amount); + } + + /// @notice Deposit ERC20 tokens into the lockbox + /// @param to The user who should received minted tokens + /// @param amount The amount of tokens to deposit + function depositTo(address to, uint256 amount) external { + _deposit(to, amount); + } + + /// @notice Deposit tokens into the lockbox + /// @param to The user who should received minted tokens + /// @param amount The amount of tokens to deposit + function _deposit(address to, uint256 amount) internal { + SEND_V0.safeTransferFrom(msg.sender, address(this), amount); + emit Deposit(to, amount); + + /// @notice v0 token has 0 decimals, v1 token has 18 decimals, therefore we multiply by 1 ether + /// @notice v0 token has 100B supply, v1 token has 1B supply, therefore divided by 100 + uint256 amountToMint = (amount * 1 ether) / 100; + SEND_V1.mint(to, amountToMint); + } +} diff --git a/contracts/SendToken.sol b/contracts/SendToken.sol new file mode 100644 index 000000000..09d86e196 --- /dev/null +++ b/contracts/SendToken.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; + +/* + + ███████╗███████╗███╗ ██╗██████╗ ██╗████████╗ + ██╔════╝██╔════╝████╗ ██║██╔══██╗ ██║╚══██╔══╝ + ███████╗█████╗ ██╔██╗ ██║██║ ██║ ██║ ██║ + ╚════██║██╔══╝ ██║╚██╗██║██║ ██║ ██║ ██║ + ███████║███████╗██║ ╚████║██████╔╝ ██║ ██║ + ╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝ + +*/ +contract SendToken is ERC20Burnable { + address public immutable lockbox; + + constructor( + string memory _name, + string memory _symbol, + address _lockbox + ) ERC20(_name, _symbol) { + require(_lockbox != address(0), "ZL"); + lockbox = _lockbox; + } + + function mint(address to, uint256 amount) external { + require(msg.sender == lockbox, "NL"); + _mint(to, amount); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 000000000..ea2a23846 --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,58 @@ +import type { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox-viem"; +import dotenv from "dotenv"; + +dotenv.config(); + +const config: HardhatUserConfig = { + solidity: "0.8.28", + networks: { + // base mainnet network + hardhat: { + chainId: 8453, + forking: { + url: "https://mainnet.base.org", + }, + }, + anvil: { + url: "http://127.0.0.1:8546", + chainId: 845337, + }, + sepolia: { + url: "https://sepolia.base.org", + chainId: 84532, + accounts: [process.env.EOA_DEPLOYER!], + }, + base: { + url: "https://mainnet.base.org", + chainId: 8453, + accounts: [process.env.EOA_DEPLOYER!], + }, + }, + etherscan: { + apiKey: { + base: process.env.ETHERSCAN_API_KEY!, + sepolia: process.env.ETHERSCAN_API_KEY!, + }, + customChains: [ + { + network: "base", + chainId: 8453, + urls: { + apiURL: "https://api.basescan.org/api", + browserURL: "https://basescan.org", + }, + }, + { + network: "sepolia", + chainId: 84532, + urls: { + apiURL: "https://api-sepolia.basescan.org/api", + browserURL: "https://sepolia.basescan.org", + }, + }, + ], + }, +}; + +export default config; diff --git a/ignition/modules/SendToken.ts b/ignition/modules/SendToken.ts new file mode 100644 index 000000000..77f30e0be --- /dev/null +++ b/ignition/modules/SendToken.ts @@ -0,0 +1,34 @@ +// 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"; + +const TOKEN_NAME = "Send"; +const TOKEN_SYMBOL = "SEND"; + +/// @dev Lockbox address is pre-calculated +const LOCKBOX_ADDRESS = "0x60E5445EDc1A469CFc0181861c88BD4B6895F615"; + +/// @dev SendV0 address on Base chain +const SEND_V0 = "0x3f14920c99beb920afa163031c4e47a3e03b3e4a"; + +/// @dev Make sure we are using deployed that pre-calculated the lockbox address +export const EOA_DEPLOYER = "0x7F314BffCB437b7046F469dE2457f9C4014931e1"; + +const SendTokenModule = buildModule("SendTokenModule", (m) => { + const sendToken = m.contract( + "SendToken", + [TOKEN_NAME, TOKEN_SYMBOL, LOCKBOX_ADDRESS], + { + from: EOA_DEPLOYER, + } + ); + + const sendLockbox = m.contract("SendLockbox", [SEND_V0, sendToken], { + from: EOA_DEPLOYER, + }); + + return { sendToken, sendLockbox }; +}); + +export default SendTokenModule; diff --git a/package.json b/package.json new file mode 100644 index 000000000..146ecf0f1 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "@0xsend/send-token-upgrade", + "version": "0.0.3", + "devDependencies": { + "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", + "@types/bun": "latest", + "hardhat": "^2.22.17", + "@openzeppelin/contracts": "^5.1.0", + "dotenv": "^16.4.7", + "zx": "^8.3.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "scripts": { + "test": "hardhat test" + }, + "files": [ + "artifacts", + "ignition", + "contracts", + "bin", + "README.md", + "package.json", + "tsconfig.json", + "hardhat.config.ts" + ] +} diff --git a/test/SendToken.ts b/test/SendToken.ts new file mode 100644 index 000000000..a629e6b21 --- /dev/null +++ b/test/SendToken.ts @@ -0,0 +1,79 @@ +import { + loadFixture, + impersonateAccount, + setBalance, + reset, +} from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; +import { expect } from "chai"; +import hre, { ignition } from "hardhat"; +import { parseEther } from "viem"; +import SendTokenModule, { EOA_DEPLOYER } from "../ignition/modules/SendToken"; + +/// @dev We use treasury to test the migration +const TREASURY_ADDRESS = "0x05CEa6C36f3a44944A4F4bA39B1820677AcB97EE"; + +describe("SendToken", 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 deployToken() { + /// @dev we reset the network to make sure we are testing prior to mainnet deployment + await reset(hre.config.networks.hardhat.forking?.url, 24820325); + await impersonateAccount(EOA_DEPLOYER); + await setBalance(EOA_DEPLOYER, parseEther("1")); + + const { sendToken, sendLockbox } = await ignition.deploy(SendTokenModule); + + return { + sendToken, + sendLockbox, + }; + } + + it("Should match the meta", async function () { + const { sendToken, sendLockbox } = await loadFixture(deployToken); + + /// @dev Make sure the addresses are correct + expect(sendToken.address).to.equal( + "0xEab49138BA2Ea6dd776220fE26b7b8E446638956" + ); + expect(sendLockbox.address).to.equal( + "0x60E5445EDc1A469CFc0181861c88BD4B6895F615" + ); + + expect(await sendToken.read.name()).to.equal("Send"); + expect(await sendToken.read.symbol()).to.equal("SEND"); + expect(await sendToken.read.decimals()).to.equal(18); + }); + + it("Should convert 100 SENDv0 to 1e18 SENDv1", async function () { + const { sendToken, sendLockbox } = await loadFixture(deployToken); + + // impersonate treasury address and approve lockbox contract + await impersonateAccount(TREASURY_ADDRESS); + await setBalance(TREASURY_ADDRESS, parseEther("1")); + + const oldToken = await hre.viem.getContractAt( + "IERC20", + "0x3f14920c99beb920afa163031c4e47a3e03b3e4a" + ); + + // approve lockbox contract + await oldToken.write.approve([sendLockbox.address, 100n], { + account: TREASURY_ADDRESS, + }); + + // deposit 100 tokens + await sendLockbox.write.deposit([100n], { + account: TREASURY_ADDRESS, + }); + + // there should be 100 SEND V0 in the lockbox + expect(await oldToken.read.balanceOf([sendLockbox.address])).to.equal(100n); + + // there should be 1 minted SEND with 18 decimals (1 ether) + expect(await sendToken.read.balanceOf([TREASURY_ADDRESS])).to.equal( + parseEther("1") + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..574e785c7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +}