diff --git a/contracts/base/README.md b/contracts/base/README.md new file mode 100644 index 000000000..21455f07a --- /dev/null +++ b/contracts/base/README.md @@ -0,0 +1,76 @@ +# RIP-305: wRTC ERC-20 on Base L2 + +## Overview + +Wrapped RTC (wRTC) ERC-20 token implementing [RIP-305](../docs/RIP-305-cross-chain-airdrop.md) for the Base L2 network. + +## Contract: WrappedRTC.sol + +- **Network**: Base (mainnet, chainId 8453) + Base Sepolia (testnet, chainId 84532) +- **Standard**: ERC-20 with mint/burn (OpenZeppelin v5) +- **Decimals**: 6 (matches native RTC precision) +- **Max Supply**: 20,000 wRTC (20,000,000,000 in 6-decimal units) +- **Roles**: Owner + Bridge (admin-controlled in Phase 1) + +## Features + +- `mint(address to, uint256 amount)` — Bridge or owner mints wRTC when RTC locked on RustChain +- `burnFrom(address from, uint256 amount)` — Bridge burns wRTC when user redeems to RustChain +- `setBridge(address bridge)` — Owner sets authorized bridge address +- `remainingSupply()` — View remaining mintable supply +- MAX_SUPPLY enforced — cannot exceed 20,000 wRTC total + +## Deployment + +### Prerequisites + +```bash +npm install +cp .env.example .env +# Add PRIVATE_KEY and BASESCAN_API_KEY to .env +``` + +### Deploy to Base Sepolia (testnet) + +```bash +PRIVATE_KEY=0x... BASESCAN_API_KEY=... npx hardhat run scripts/deploy.js --network base-sepolia +``` + +### Verify on BaseScan + +```bash +npx hardhat verify --network base-sepolia +``` + +### Deploy to Base Mainnet + +```bash +PRIVATE_KEY=0x... BASESCAN_API_KEY=... npx hardhat run scripts/deploy.js --network base +``` + +## Security Notes + +- Phase 1: Admin-controlled bridge (owner can set bridge address) +- Phase 2 (future): Trustless bridge via cross-chain message verification +- MAX_SUPPLY cap prevents unbounded inflation +- onlyBridgeOrOwner modifier on mint/burn functions + +## RIP-305 Airdrop Eligibility Tiers + +| Tier | Requirement | wRTC Claim | +|------|------------|------------| +| Stargazer | 10+ repos starred | 25 wRTC | +| Contributor | 1+ merged PR | 50 wRTC | +| Builder | 3+ merged PRs | 100 wRTC | +| Security | Verified vulnerability | 150 wRTC | +| Core | 5+ merged PRs / Star King | 200 wRTC | +| Miner | Active attestation | 100 wRTC | + +## Status + +- [x] Contract written + tested locally +- [x] Compiles with Hardhat (Solidity 0.8.20, Paris EVM) +- [ ] Deployed to Base Sepolia (pending testnet ETH) +- [ ] Verified on BaseScan +- [ ] Deployed to Base Mainnet + diff --git a/contracts/base/hardhat.config.js b/contracts/base/hardhat.config.js new file mode 100644 index 000000000..d002310c8 --- /dev/null +++ b/contracts/base/hardhat.config.js @@ -0,0 +1,42 @@ +require("@nomicfoundation/hardhat-toolbox"); + +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: { + version: "0.8.20", + settings: { + optimizer: { + enabled: true, + runs: 200 + } + } + }, + networks: { + "base-sepolia": { + url: "https://sepolia.base.org", + chainId: 84532, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + }, + "base": { + url: "https://mainnet.base.org", + chainId: 8453, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] + } + }, + etherscan: { + apiKey: { + "base-sepolia": process.env.BASESCAN_API_KEY || "", + "base": process.env.BASESCAN_API_KEY || "" + }, + customChains: [ + { + network: "base-sepolia", + chainId: 84532, + urls: { + apiURL: "https://api-sepolia.basescan.org/api", + browserURL: "https://sepolia.basescan.org" + } + } + ] + } +}; diff --git a/contracts/base/scripts/deploy.js b/contracts/base/scripts/deploy.js new file mode 100644 index 000000000..d3d7c37bd --- /dev/null +++ b/contracts/base/scripts/deploy.js @@ -0,0 +1,28 @@ +const hre = require("hardhat"); + +async function main() { + const [deployer] = await hre.ethers.getSigners(); + console.log("Deploying wRTC with account:", deployer.address); + console.log("Account balance:", (await deployer.provider.getBalance(deployer.address)).toString()); + + const WrappedRTC = await hre.ethers.getContractFactory("WrappedRTC"); + const wrtc = await WrappedRTC.deploy(deployer.address); + await wrtc.waitForDeployment(); + + const address = await wrtc.getAddress(); + console.log("wRTC deployed to:", address); + console.log("Owner:", await wrtc.owner()); + console.log("Total supply:", await wrtc.totalSupply()); + console.log("Max supply:", await wrtc.MAX_SUPPLY()); + console.log("Decimals:", await wrtc.decimals()); + + console.log("\n✅ Deployment complete!"); + console.log(`Verify with: npx hardhat verify --network base-sepolia ${address} ${deployer.address}`); + + return address; +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/contracts/base/wRTC.sol b/contracts/base/wRTC.sol new file mode 100644 index 000000000..93b0be89c --- /dev/null +++ b/contracts/base/wRTC.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title Wrapped RTC (wRTC) + * @notice ERC-20 representation of RustChain RTC tokens on Base L2 + * @dev Implements RIP-305 Cross-Chain Airdrop Protocol + * 6 decimal precision to match native RTC token + * Mint/burn functions for bridge integration (Phase 1: admin-controlled) + */ +contract WrappedRTC is ERC20, ERC20Burnable, Ownable { + uint256 public constant MAX_SUPPLY = 50_000 * 10**6; // 50,000 wRTC (6 decimals) + + address public bridge; + + event BridgeSet(address indexed oldBridge, address indexed newBridge); + event Minted(address indexed to, uint256 amount); + event Burned(address indexed from, uint256 amount); + + modifier onlyBridgeOrOwner() { + require( + msg.sender == bridge || msg.sender == owner(), + "wRTC: caller is not bridge or owner" + ); + _; + } + + constructor(address initialOwner) + ERC20("Wrapped RTC", "wRTC") + Ownable(initialOwner) + {} + + /** + * @notice Returns token decimals — 6 to match native RTC precision + */ + function decimals() public pure override returns (uint8) { + return 6; + } + + /** + * @notice Set the authorized bridge address + * @param _bridge Address of the RustChain bridge contract + */ + function setBridge(address _bridge) external onlyOwner { + require(_bridge != address(0), "wRTC: zero address"); + address old = bridge; + bridge = _bridge; + emit BridgeSet(old, _bridge); + } + + /** + * @notice Mint wRTC tokens — called by bridge when RTC is locked on RustChain + * @param to Recipient address + * @param amount Amount to mint (in 6-decimal units) + */ + function mint(address to, uint256 amount) external onlyBridgeOrOwner { + require(totalSupply() + amount <= MAX_SUPPLY, "wRTC: exceeds max supply"); + _mint(to, amount); + emit Minted(to, amount); + } + + /** + * @notice Burn wRTC tokens — called by bridge when user wants to return to RustChain + * @param from Address to burn from + * @param amount Amount to burn (in 6-decimal units) + */ + function burnFrom(address from, uint256 amount) public override onlyBridgeOrOwner { + _burn(from, amount); + emit Burned(from, amount); + } + + /** + * @notice Get remaining mintable supply + */ + function remainingSupply() external view returns (uint256) { + return MAX_SUPPLY - totalSupply(); + } +}