mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 18:31:41 +00:00
@@ -0,0 +1,15 @@
|
||||
# Sonr EVM Configuration
|
||||
# Local development (uses localchain_9000-1)
|
||||
SONR_RPC_URL=http://localhost:8545
|
||||
|
||||
# Testnet (when available, uses sonr-testnet-1)
|
||||
SONR_TESTNET_RPC_URL=http://localhost:8545
|
||||
|
||||
# Private key for deployment (DO NOT COMMIT REAL KEYS)
|
||||
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
|
||||
# Block explorer API key for contract verification
|
||||
ETHERSCAN_API_KEY=
|
||||
|
||||
# Alchemy API key (optional, for Ethereum testnet comparison)
|
||||
ALCHEMY_API_KEY=
|
||||
@@ -0,0 +1,19 @@
|
||||
# Foundry files
|
||||
cache/
|
||||
out/
|
||||
broadcast/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Dependencies
|
||||
lib/
|
||||
|
||||
# Coverage
|
||||
lcov.info
|
||||
coverage/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.claude*
|
||||
@@ -0,0 +1,153 @@
|
||||
# WSNR Deployment Scripts
|
||||
|
||||
This directory contains multiple deployment scripts for the WSNR (Wrapped SNR) smart contract.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Compile the contract first**:
|
||||
|
||||
```bash
|
||||
cd contracts
|
||||
forge build
|
||||
```
|
||||
|
||||
2. **Set up environment variables**:
|
||||
|
||||
```bash
|
||||
cd contracts
|
||||
cp .env.example .env
|
||||
# Edit .env and add your PRIVATE_KEY
|
||||
```
|
||||
|
||||
3. **Ensure your Sonr node is running** with EVM enabled on `http://localhost:8545`
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Option 1: Foundry Script (Recommended)
|
||||
|
||||
The most robust option using Foundry's native scripting:
|
||||
|
||||
```bash
|
||||
# Deploy to local Sonr node
|
||||
./scripts/deploy-wsnr.sh
|
||||
|
||||
# Deploy to testnet
|
||||
./scripts/deploy-wsnr.sh sonr-testnet
|
||||
```
|
||||
|
||||
Features:
|
||||
|
||||
- Automatic chain detection
|
||||
- Balance checking
|
||||
- Contract verification
|
||||
- Deployment info saved to `contracts/deployments/`
|
||||
|
||||
### Option 2: Simple Node.js Script
|
||||
|
||||
Requires Node.js and ethers.js:
|
||||
|
||||
```bash
|
||||
# Install dependencies (if not already installed)
|
||||
npm install ethers
|
||||
|
||||
# Deploy
|
||||
node scripts/deploy-wsnr-simple.js [rpc-url]
|
||||
|
||||
# Example with custom RPC
|
||||
node scripts/deploy-wsnr-simple.js http://192.168.1.100:8545
|
||||
```
|
||||
|
||||
### Option 3: Python Script
|
||||
|
||||
Requires Python 3 and web3.py:
|
||||
|
||||
```bash
|
||||
# Install dependencies (if not already installed)
|
||||
pip3 install web3 eth-account
|
||||
|
||||
# Deploy
|
||||
python3 scripts/deploy-wsnr.py [rpc-url]
|
||||
|
||||
# Example with custom RPC
|
||||
python3 scripts/deploy-wsnr.py http://192.168.1.100:8545
|
||||
```
|
||||
|
||||
### Option 4: Direct Foundry Command
|
||||
|
||||
For advanced users who want to customize deployment:
|
||||
|
||||
```bash
|
||||
cd contracts
|
||||
forge script script/DeployWSNR.s.sol:DeployWSNR \
|
||||
--rpc-url http://localhost:8545 \
|
||||
--broadcast \
|
||||
--private-key $PRIVATE_KEY
|
||||
```
|
||||
|
||||
## Post-Deployment
|
||||
|
||||
After deployment, you'll receive:
|
||||
|
||||
- Contract address
|
||||
- Transaction hash
|
||||
- Block number
|
||||
- Deployment info saved to `contracts/deployments/{chainId}-WSNR.json`
|
||||
|
||||
### Interacting with the Contract
|
||||
|
||||
**Using cast (Foundry)**:
|
||||
|
||||
```bash
|
||||
# Deposit SNR to get WSNR
|
||||
cast send <CONTRACT_ADDRESS> "deposit()" --value 1ether --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY
|
||||
|
||||
# Check WSNR balance
|
||||
cast call <CONTRACT_ADDRESS> "balanceOf(address)" <YOUR_ADDRESS> --rpc-url http://localhost:8545
|
||||
|
||||
# Withdraw SNR
|
||||
cast send <CONTRACT_ADDRESS> "withdraw(uint256)" 1000000000000000000 --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY
|
||||
```
|
||||
|
||||
**Using web3 console**:
|
||||
|
||||
```javascript
|
||||
// Connect to contract
|
||||
const wsnr = new web3.eth.Contract(abi, contractAddress);
|
||||
|
||||
// Deposit
|
||||
await wsnr.methods
|
||||
.deposit()
|
||||
.send({ from: account, value: web3.utils.toWei("1", "ether") });
|
||||
|
||||
// Check balance
|
||||
const balance = await wsnr.methods.balanceOf(account).call();
|
||||
|
||||
// Withdraw
|
||||
await wsnr.methods
|
||||
.withdraw(web3.utils.toWei("1", "ether"))
|
||||
.send({ from: account });
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot connect to node
|
||||
|
||||
- Ensure Sonr node is running: `make sh-testnet` or `docker-compose up sonr-node`
|
||||
- Check if EVM is enabled with JSON-RPC on port 8545
|
||||
- Try `curl http://localhost:8545` to test connectivity
|
||||
|
||||
### Insufficient balance
|
||||
|
||||
- Fund your deployer address with SNR tokens
|
||||
- Check balance: `cast balance <YOUR_ADDRESS> --rpc-url http://localhost:8545`
|
||||
|
||||
### Contract not compiled
|
||||
|
||||
- Run `cd contracts && forge build` first
|
||||
- Ensure you have Foundry installed: `curl -L https://foundry.paradigm.xyz | bash`
|
||||
|
||||
### Transaction fails
|
||||
|
||||
- Check gas prices and limits
|
||||
- Ensure your account has enough SNR for gas
|
||||
- Check if the network is synced
|
||||
@@ -0,0 +1,105 @@
|
||||
# Foundry Makefile for Sonr Smart Contracts
|
||||
|
||||
# Load environment variables
|
||||
-include .env
|
||||
|
||||
# Default network
|
||||
NETWORK ?= sonr
|
||||
|
||||
# Contract verification
|
||||
VERIFIER ?= etherscan
|
||||
VERIFIER_URL ?= https://api.etherscan.io/api
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help message
|
||||
@gum log --level info "Sonr Smart Contracts - Foundry Commands"
|
||||
@gum log --level info ""
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: install
|
||||
install: ## Install Foundry and dependencies
|
||||
curl -L https://foundry.paradigm.xyz | bash
|
||||
foundryup
|
||||
forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build contracts
|
||||
forge build
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run tests
|
||||
forge test -vvv
|
||||
|
||||
.PHONY: test-gas
|
||||
test-gas: ## Run tests with gas reporting
|
||||
forge test -vvv --gas-report
|
||||
|
||||
.PHONY: coverage
|
||||
coverage: ## Generate test coverage report
|
||||
forge coverage
|
||||
|
||||
.PHONY: format
|
||||
format: ## Format Solidity code
|
||||
forge fmt
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Lint Solidity code
|
||||
forge fmt --check
|
||||
|
||||
.PHONY: snapshot
|
||||
snapshot: ## Create gas snapshot
|
||||
forge snapshot
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Clean build artifacts
|
||||
forge clean
|
||||
|
||||
# Deployment commands
|
||||
.PHONY: deploy-wsnr
|
||||
deploy-wsnr: ## Deploy WSNR contract
|
||||
@gum log --level info "Deploying WSNR to $(NETWORK)..."
|
||||
forge script script/DeployWSNR.s.sol:DeployWSNR \
|
||||
--rpc-url $(NETWORK) \
|
||||
--broadcast \
|
||||
--verify \
|
||||
-vvvv
|
||||
|
||||
.PHONY: deploy-local
|
||||
deploy-local: ## Deploy to local Sonr node
|
||||
@gum log --level info "Deploying to local Sonr node..."
|
||||
@cd .. && ./scripts/deploy-wsnr.sh sonr
|
||||
|
||||
.PHONY: deploy-testnet
|
||||
deploy-testnet: ## Deploy to Sonr testnet
|
||||
@gum log --level info "Deploying to Sonr testnet..."
|
||||
@cd .. && ./scripts/deploy-wsnr.sh sonr-testnet
|
||||
|
||||
.PHONY: deploy
|
||||
deploy: deploy-local ## Default deployment (alias for deploy-local)
|
||||
|
||||
# Interaction commands
|
||||
.PHONY: console
|
||||
console: ## Start Foundry console
|
||||
forge console --rpc-url $(NETWORK)
|
||||
|
||||
.PHONY: verify
|
||||
verify: ## Verify contract on Etherscan
|
||||
forge verify-contract \
|
||||
--chain-id $(shell cast chain-id --rpc-url $(NETWORK)) \
|
||||
--etherscan-api-key $(ETHERSCAN_API_KEY) \
|
||||
--verifier $(VERIFIER) \
|
||||
$(CONTRACT_ADDRESS) \
|
||||
$(CONTRACT_NAME)
|
||||
|
||||
# Development helpers
|
||||
.PHONY: anvil
|
||||
anvil: ## Start local Anvil node
|
||||
anvil --chain-id 31337
|
||||
|
||||
.PHONY: cast-balance
|
||||
cast-balance: ## Check ETH balance of an address
|
||||
@cast balance $(ADDRESS) --rpc-url $(NETWORK)
|
||||
|
||||
.PHONY: cast-send
|
||||
cast-send: ## Send a transaction
|
||||
@cast send $(TO) --value $(VALUE) --rpc-url $(NETWORK) --private-key $(PRIVATE_KEY)
|
||||
@@ -0,0 +1,122 @@
|
||||
# Sonr Smart Contracts
|
||||
|
||||
This directory contains the smart contracts for the Sonr blockchain, including the WSNR (Wrapped SNR) ERC-20 token contract.
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Install Foundry:
|
||||
|
||||
```bash
|
||||
curl -L https://foundry.paradigm.xyz | bash
|
||||
foundryup
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
3. Copy environment variables:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. Configure your `.env` file with appropriate values
|
||||
|
||||
## Development
|
||||
|
||||
### Build contracts:
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### Run tests:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
### Run tests with gas reporting:
|
||||
|
||||
```bash
|
||||
make test-gas
|
||||
```
|
||||
|
||||
### Generate coverage report:
|
||||
|
||||
```bash
|
||||
make coverage
|
||||
```
|
||||
|
||||
### Format code:
|
||||
|
||||
```bash
|
||||
make format
|
||||
```
|
||||
|
||||
### Deploy to local Sonr network:
|
||||
|
||||
```bash
|
||||
make deploy-local
|
||||
```
|
||||
|
||||
### Deploy to Sonr testnet:
|
||||
|
||||
```bash
|
||||
make deploy-testnet
|
||||
```
|
||||
|
||||
### Clean build artifacts:
|
||||
|
||||
```bash
|
||||
make clean
|
||||
```
|
||||
|
||||
### Start local Anvil node (for testing):
|
||||
|
||||
```bash
|
||||
make anvil
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
contracts/
|
||||
├── src/ # Contract source files
|
||||
├── test/ # Test files
|
||||
├── script/ # Deployment scripts
|
||||
├── lib/ # Dependencies (gitignored)
|
||||
├── foundry.toml # Foundry configuration
|
||||
└── Makefile # Build commands
|
||||
```
|
||||
|
||||
## Testing on Sonr Native EVM
|
||||
|
||||
Start the local testnet with EVM enabled:
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
docker-compose -f docker-compose.dev.yml up -d sonr-node
|
||||
```
|
||||
|
||||
The EVM JSON-RPC endpoint will be available at:
|
||||
|
||||
- HTTP: http://localhost:8545
|
||||
- WebSocket: ws://localhost:8546
|
||||
|
||||
## Foundry Commands
|
||||
|
||||
For a full list of available commands:
|
||||
|
||||
```bash
|
||||
make help
|
||||
```
|
||||
|
||||
## Contract Addresses
|
||||
|
||||
- WSNR: TBD (after deployment)
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔧 Setting up Foundry environment..."
|
||||
|
||||
# Source foundry if installed via foundryup
|
||||
if [ -f "$HOME/.foundry/bin/forge" ]; then
|
||||
export PATH="$HOME/.foundry/bin:$PATH"
|
||||
fi
|
||||
|
||||
# Check if forge is available
|
||||
if ! command -v forge &>/dev/null; then
|
||||
echo "❌ Forge not found in PATH. Please ensure Foundry is installed:"
|
||||
echo " curl -L https://foundry.paradigm.xyz | bash"
|
||||
echo " foundryup"
|
||||
echo ""
|
||||
echo "Then run: source ~/.bashrc or source ~/.zshrc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Forge found at: $(which forge)"
|
||||
echo "📦 Installing OpenZeppelin contracts..."
|
||||
|
||||
# Install OpenZeppelin if not already installed
|
||||
if [ ! -d "lib/openzeppelin-contracts" ]; then
|
||||
forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit
|
||||
else
|
||||
echo "✅ OpenZeppelin already installed"
|
||||
fi
|
||||
|
||||
echo "🏗️ Building contracts..."
|
||||
forge build
|
||||
|
||||
echo "✅ Build complete!"
|
||||
@@ -0,0 +1,25 @@
|
||||
[profile.default]
|
||||
src = "src"
|
||||
out = "out"
|
||||
libs = ["lib"]
|
||||
solc = "0.8.20"
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
|
||||
# Network configurations
|
||||
[rpc_endpoints]
|
||||
sonr = "${SONR_RPC_URL}"
|
||||
sonr_testnet = "${SONR_TESTNET_RPC_URL}"
|
||||
sepolia = "https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
|
||||
|
||||
[etherscan]
|
||||
sonr = { key = "${ETHERSCAN_API_KEY}" }
|
||||
|
||||
# Testing configuration
|
||||
[fuzz]
|
||||
runs = 256
|
||||
|
||||
[invariant]
|
||||
runs = 256
|
||||
depth = 128
|
||||
fail_on_revert = false
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Script, console2} from "forge-std/Script.sol";
|
||||
import {WSNR} from "../src/WSNR.sol";
|
||||
|
||||
contract DeployWSNR is Script {
|
||||
function run() external returns (WSNR) {
|
||||
// Get deployer private key from environment
|
||||
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
|
||||
|
||||
// Get chain ID for logging
|
||||
uint256 chainId = block.chainid;
|
||||
|
||||
console2.log("Deploying WSNR to chain ID:", chainId);
|
||||
console2.log("Deployer address:", vm.addr(deployerPrivateKey));
|
||||
console2.log("Deployer balance:", vm.addr(deployerPrivateKey).balance);
|
||||
|
||||
// Start broadcasting transactions
|
||||
vm.startBroadcast(deployerPrivateKey);
|
||||
|
||||
// Deploy WSNR contract
|
||||
WSNR wsnr = new WSNR();
|
||||
|
||||
console2.log("WSNR deployed at:", address(wsnr));
|
||||
console2.log("Contract name:", wsnr.name());
|
||||
console2.log("Contract symbol:", wsnr.symbol());
|
||||
console2.log("Contract decimals:", wsnr.decimals());
|
||||
|
||||
vm.stopBroadcast();
|
||||
|
||||
// Write deployment info to file for reference
|
||||
string memory deploymentInfo = string(
|
||||
abi.encodePacked(
|
||||
"{\n",
|
||||
' "contractName": "WSNR",\n',
|
||||
' "address": "', vm.toString(address(wsnr)), '",\n',
|
||||
' "chainId": ', vm.toString(chainId), ',\n',
|
||||
' "deployer": "', vm.toString(vm.addr(deployerPrivateKey)), '",\n',
|
||||
' "deploymentBlock": ', vm.toString(block.number), ',\n',
|
||||
' "timestamp": ', vm.toString(block.timestamp), '\n',
|
||||
"}"
|
||||
)
|
||||
);
|
||||
|
||||
// Save deployment info
|
||||
vm.writeFile(
|
||||
string(abi.encodePacked("deployments/", vm.toString(chainId), "-WSNR.json")),
|
||||
deploymentInfo
|
||||
);
|
||||
|
||||
return wsnr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
|
||||
import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
|
||||
import {IWSNR} from "./interfaces/IWSNR.sol";
|
||||
|
||||
/**
|
||||
* @title WSNR - Wrapped SNR
|
||||
* @notice ERC20 wrapper for native SNR tokens
|
||||
* @dev Implements a 1:1 wrapping mechanism for SNR tokens with deposit/withdraw functionality
|
||||
*/
|
||||
contract WSNR is IWSNR, ERC20, ReentrancyGuard {
|
||||
/**
|
||||
* @notice Initializes the WSNR token contract
|
||||
* @dev Sets token name as "Wrapped SNR" and symbol as "WSNR"
|
||||
*/
|
||||
constructor() ERC20("Wrapped SNR", "WSNR") {}
|
||||
|
||||
/**
|
||||
* @notice Fallback function to handle direct SNR transfers
|
||||
* @dev Automatically wraps sent SNR into WSNR tokens
|
||||
*/
|
||||
receive() external payable {
|
||||
deposit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Deposit native SNR and receive WSNR tokens
|
||||
* @dev Mints WSNR tokens equal to the amount of SNR sent
|
||||
*/
|
||||
function deposit() public payable override nonReentrant {
|
||||
require(msg.value > 0, "WSNR: deposit amount must be greater than 0");
|
||||
|
||||
_mint(msg.sender, msg.value);
|
||||
|
||||
emit Deposit(msg.sender, msg.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Deposit native SNR to a specific address
|
||||
* @param to Address to receive the WSNR tokens
|
||||
* @dev Allows depositing on behalf of another address
|
||||
*/
|
||||
function depositTo(address to) public payable override nonReentrant {
|
||||
require(msg.value > 0, "WSNR: deposit amount must be greater than 0");
|
||||
require(to != address(0), "WSNR: cannot deposit to zero address");
|
||||
|
||||
_mint(to, msg.value);
|
||||
|
||||
emit Deposit(to, msg.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraw native SNR by burning WSNR tokens
|
||||
* @param amount Amount of WSNR to burn and SNR to receive
|
||||
* @dev Burns WSNR tokens and sends equivalent native SNR
|
||||
*/
|
||||
function withdraw(uint256 amount) public override {
|
||||
withdrawTo(msg.sender, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraw native SNR to a specific address
|
||||
* @param to Address to receive the native SNR
|
||||
* @param amount Amount of WSNR to burn
|
||||
* @dev Allows withdrawing to a different address
|
||||
*/
|
||||
function withdrawTo(address to, uint256 amount) public override nonReentrant {
|
||||
require(amount > 0, "WSNR: withdrawal amount must be greater than 0");
|
||||
require(to != address(0), "WSNR: cannot withdraw to zero address");
|
||||
require(balanceOf(msg.sender) >= amount, "WSNR: insufficient balance");
|
||||
|
||||
_burn(msg.sender, amount);
|
||||
|
||||
(bool success,) = to.call{value: amount}("");
|
||||
require(success, "WSNR: SNR transfer failed");
|
||||
|
||||
emit Withdrawal(to, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the total amount of SNR locked in the contract
|
||||
* @return The balance of native SNR held by the contract
|
||||
*/
|
||||
function getReserve() public view returns (uint256) {
|
||||
return address(this).balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify that total supply equals contract balance
|
||||
* @dev This should always return true for proper 1:1 backing
|
||||
* @return Whether the contract is properly collateralized
|
||||
*/
|
||||
function isFullyCollateralized() public view returns (bool) {
|
||||
return totalSupply() == address(this).balance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
/**
|
||||
* @title IWSNR
|
||||
* @notice Interface for Wrapped SNR (WSNR) token contract
|
||||
* @dev Extends ERC20 with deposit and withdraw functionality for wrapping native SNR tokens
|
||||
*/
|
||||
interface IWSNR is IERC20 {
|
||||
/**
|
||||
* @notice Emitted when native SNR is deposited and WSNR is minted
|
||||
* @param from Address that deposited SNR
|
||||
* @param amount Amount of SNR deposited (and WSNR minted)
|
||||
*/
|
||||
event Deposit(address indexed from, uint256 amount);
|
||||
|
||||
/**
|
||||
* @notice Emitted when WSNR is burned and native SNR is withdrawn
|
||||
* @param to Address that received SNR
|
||||
* @param amount Amount of WSNR burned (and SNR withdrawn)
|
||||
*/
|
||||
event Withdrawal(address indexed to, uint256 amount);
|
||||
|
||||
/**
|
||||
* @notice Deposit native SNR and receive WSNR tokens
|
||||
* @dev Mints WSNR tokens equal to the amount of SNR sent
|
||||
*/
|
||||
function deposit() external payable;
|
||||
|
||||
/**
|
||||
* @notice Withdraw native SNR by burning WSNR tokens
|
||||
* @param amount Amount of WSNR to burn and SNR to receive
|
||||
* @dev Burns WSNR tokens and sends equivalent native SNR
|
||||
*/
|
||||
function withdraw(uint256 amount) external;
|
||||
|
||||
/**
|
||||
* @notice Deposit native SNR to a specific address
|
||||
* @param to Address to receive the WSNR tokens
|
||||
* @dev Allows depositing on behalf of another address
|
||||
*/
|
||||
function depositTo(address to) external payable;
|
||||
|
||||
/**
|
||||
* @notice Withdraw native SNR to a specific address
|
||||
* @param to Address to receive the native SNR
|
||||
* @param amount Amount of WSNR to burn
|
||||
* @dev Allows withdrawing to a different address
|
||||
*/
|
||||
function withdrawTo(address to, uint256 amount) external;
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test, console2} from "forge-std/Test.sol";
|
||||
import {WSNR} from "../src/WSNR.sol";
|
||||
import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
contract WSNRTest is Test {
|
||||
WSNR public wsnr;
|
||||
address public alice = address(0x1);
|
||||
address public bob = address(0x2);
|
||||
address public charlie = address(0x3);
|
||||
|
||||
event Deposit(address indexed from, uint256 amount);
|
||||
event Withdrawal(address indexed to, uint256 amount);
|
||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
|
||||
function setUp() public {
|
||||
wsnr = new WSNR();
|
||||
|
||||
// Fund test accounts
|
||||
vm.deal(alice, 100 ether);
|
||||
vm.deal(bob, 100 ether);
|
||||
vm.deal(charlie, 100 ether);
|
||||
}
|
||||
|
||||
function testInitialState() public {
|
||||
assertEq(wsnr.name(), "Wrapped SNR");
|
||||
assertEq(wsnr.symbol(), "WSNR");
|
||||
assertEq(wsnr.decimals(), 18);
|
||||
assertEq(wsnr.totalSupply(), 0);
|
||||
assertEq(wsnr.getReserve(), 0);
|
||||
assertTrue(wsnr.isFullyCollateralized());
|
||||
}
|
||||
|
||||
function testDeposit() public {
|
||||
uint256 depositAmount = 10 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
|
||||
// Test deposit event
|
||||
vm.expectEmit(true, false, false, true);
|
||||
emit Deposit(alice, depositAmount);
|
||||
|
||||
// Deposit SNR
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
|
||||
// Check balances
|
||||
assertEq(wsnr.balanceOf(alice), depositAmount);
|
||||
assertEq(wsnr.totalSupply(), depositAmount);
|
||||
assertEq(wsnr.getReserve(), depositAmount);
|
||||
assertEq(address(wsnr).balance, depositAmount);
|
||||
assertTrue(wsnr.isFullyCollateralized());
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testDepositZeroAmount() public {
|
||||
vm.startPrank(alice);
|
||||
vm.expectRevert("WSNR: deposit amount must be greater than 0");
|
||||
wsnr.deposit{value: 0}();
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testDepositTo() public {
|
||||
uint256 depositAmount = 5 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
|
||||
// Test deposit event
|
||||
vm.expectEmit(true, false, false, true);
|
||||
emit Deposit(bob, depositAmount);
|
||||
|
||||
// Deposit SNR to bob's account
|
||||
wsnr.depositTo{value: depositAmount}(bob);
|
||||
|
||||
// Check balances
|
||||
assertEq(wsnr.balanceOf(alice), 0);
|
||||
assertEq(wsnr.balanceOf(bob), depositAmount);
|
||||
assertEq(wsnr.totalSupply(), depositAmount);
|
||||
assertEq(address(wsnr).balance, depositAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testDepositToZeroAddress() public {
|
||||
vm.startPrank(alice);
|
||||
vm.expectRevert("WSNR: cannot deposit to zero address");
|
||||
wsnr.depositTo{value: 1 ether}(address(0));
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testReceiveFallback() public {
|
||||
uint256 depositAmount = 3 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
|
||||
// Test deposit event through fallback
|
||||
vm.expectEmit(true, false, false, true);
|
||||
emit Deposit(alice, depositAmount);
|
||||
|
||||
// Send SNR directly to contract
|
||||
(bool success,) = address(wsnr).call{value: depositAmount}("");
|
||||
assertTrue(success);
|
||||
|
||||
// Check balances
|
||||
assertEq(wsnr.balanceOf(alice), depositAmount);
|
||||
assertEq(wsnr.totalSupply(), depositAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testWithdraw() public {
|
||||
uint256 depositAmount = 10 ether;
|
||||
uint256 withdrawAmount = 6 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
|
||||
// First deposit
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
uint256 aliceBalanceBefore = alice.balance;
|
||||
|
||||
// Test withdrawal event
|
||||
vm.expectEmit(true, false, false, true);
|
||||
emit Withdrawal(alice, withdrawAmount);
|
||||
|
||||
// Withdraw
|
||||
wsnr.withdraw(withdrawAmount);
|
||||
|
||||
// Check balances
|
||||
assertEq(wsnr.balanceOf(alice), depositAmount - withdrawAmount);
|
||||
assertEq(wsnr.totalSupply(), depositAmount - withdrawAmount);
|
||||
assertEq(address(wsnr).balance, depositAmount - withdrawAmount);
|
||||
assertEq(alice.balance, aliceBalanceBefore + withdrawAmount);
|
||||
assertTrue(wsnr.isFullyCollateralized());
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testWithdrawAll() public {
|
||||
uint256 depositAmount = 10 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
|
||||
// Deposit and withdraw all
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
uint256 aliceBalanceBefore = alice.balance;
|
||||
|
||||
wsnr.withdraw(depositAmount);
|
||||
|
||||
// Check everything is back to zero
|
||||
assertEq(wsnr.balanceOf(alice), 0);
|
||||
assertEq(wsnr.totalSupply(), 0);
|
||||
assertEq(address(wsnr).balance, 0);
|
||||
assertEq(alice.balance, aliceBalanceBefore + depositAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testWithdrawZeroAmount() public {
|
||||
vm.startPrank(alice);
|
||||
wsnr.deposit{value: 1 ether}();
|
||||
|
||||
vm.expectRevert("WSNR: withdrawal amount must be greater than 0");
|
||||
wsnr.withdraw(0);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testWithdrawInsufficientBalance() public {
|
||||
vm.startPrank(alice);
|
||||
wsnr.deposit{value: 5 ether}();
|
||||
|
||||
vm.expectRevert("WSNR: insufficient balance");
|
||||
wsnr.withdraw(10 ether);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testWithdrawTo() public {
|
||||
uint256 depositAmount = 10 ether;
|
||||
uint256 withdrawAmount = 4 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
|
||||
// Deposit from alice
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
uint256 bobBalanceBefore = bob.balance;
|
||||
|
||||
// Test withdrawal event
|
||||
vm.expectEmit(true, false, false, true);
|
||||
emit Withdrawal(bob, withdrawAmount);
|
||||
|
||||
// Withdraw to bob
|
||||
wsnr.withdrawTo(bob, withdrawAmount);
|
||||
|
||||
// Check balances
|
||||
assertEq(wsnr.balanceOf(alice), depositAmount - withdrawAmount);
|
||||
assertEq(bob.balance, bobBalanceBefore + withdrawAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testWithdrawToZeroAddress() public {
|
||||
vm.startPrank(alice);
|
||||
wsnr.deposit{value: 1 ether}();
|
||||
|
||||
vm.expectRevert("WSNR: cannot withdraw to zero address");
|
||||
wsnr.withdrawTo(address(0), 1 ether);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testERC20Transfer() public {
|
||||
uint256 depositAmount = 10 ether;
|
||||
uint256 transferAmount = 3 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
|
||||
// Test transfer event
|
||||
vm.expectEmit(true, true, false, true);
|
||||
emit Transfer(alice, bob, transferAmount);
|
||||
|
||||
// Transfer WSNR tokens
|
||||
assertTrue(wsnr.transfer(bob, transferAmount));
|
||||
|
||||
// Check balances
|
||||
assertEq(wsnr.balanceOf(alice), depositAmount - transferAmount);
|
||||
assertEq(wsnr.balanceOf(bob), transferAmount);
|
||||
assertEq(wsnr.totalSupply(), depositAmount); // Total supply unchanged
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testERC20Approve() public {
|
||||
uint256 depositAmount = 10 ether;
|
||||
uint256 approveAmount = 5 ether;
|
||||
|
||||
vm.startPrank(alice);
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
|
||||
// Test approval event
|
||||
vm.expectEmit(true, true, false, true);
|
||||
emit Approval(alice, bob, approveAmount);
|
||||
|
||||
// Approve bob to spend alice's WSNR
|
||||
assertTrue(wsnr.approve(bob, approveAmount));
|
||||
assertEq(wsnr.allowance(alice, bob), approveAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testERC20TransferFrom() public {
|
||||
uint256 depositAmount = 10 ether;
|
||||
uint256 approveAmount = 6 ether;
|
||||
uint256 transferAmount = 4 ether;
|
||||
|
||||
// Alice deposits and approves bob
|
||||
vm.startPrank(alice);
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
wsnr.approve(bob, approveAmount);
|
||||
vm.stopPrank();
|
||||
|
||||
// Bob transfers from alice to charlie
|
||||
vm.startPrank(bob);
|
||||
|
||||
// Test transfer event
|
||||
vm.expectEmit(true, true, false, true);
|
||||
emit Transfer(alice, charlie, transferAmount);
|
||||
|
||||
assertTrue(wsnr.transferFrom(alice, charlie, transferAmount));
|
||||
vm.stopPrank();
|
||||
|
||||
// Check balances and allowance
|
||||
assertEq(wsnr.balanceOf(alice), depositAmount - transferAmount);
|
||||
assertEq(wsnr.balanceOf(charlie), transferAmount);
|
||||
assertEq(wsnr.allowance(alice, bob), approveAmount - transferAmount);
|
||||
}
|
||||
|
||||
function testMultipleUsersDepositWithdraw() public {
|
||||
// Multiple users deposit
|
||||
vm.prank(alice);
|
||||
wsnr.deposit{value: 5 ether}();
|
||||
|
||||
vm.prank(bob);
|
||||
wsnr.deposit{value: 3 ether}();
|
||||
|
||||
vm.prank(charlie);
|
||||
wsnr.deposit{value: 2 ether}();
|
||||
|
||||
// Check total supply and reserves
|
||||
assertEq(wsnr.totalSupply(), 10 ether);
|
||||
assertEq(wsnr.getReserve(), 10 ether);
|
||||
assertTrue(wsnr.isFullyCollateralized());
|
||||
|
||||
// Users withdraw
|
||||
vm.prank(alice);
|
||||
wsnr.withdraw(2 ether);
|
||||
|
||||
vm.prank(bob);
|
||||
wsnr.withdraw(1 ether);
|
||||
|
||||
// Check final state
|
||||
assertEq(wsnr.totalSupply(), 7 ether);
|
||||
assertEq(wsnr.getReserve(), 7 ether);
|
||||
assertEq(wsnr.balanceOf(alice), 3 ether);
|
||||
assertEq(wsnr.balanceOf(bob), 2 ether);
|
||||
assertEq(wsnr.balanceOf(charlie), 2 ether);
|
||||
assertTrue(wsnr.isFullyCollateralized());
|
||||
}
|
||||
|
||||
// Fuzz testing
|
||||
function testFuzzDeposit(uint256 amount) public {
|
||||
vm.assume(amount > 0 && amount <= 100 ether);
|
||||
|
||||
vm.deal(alice, amount);
|
||||
vm.prank(alice);
|
||||
wsnr.deposit{value: amount}();
|
||||
|
||||
assertEq(wsnr.balanceOf(alice), amount);
|
||||
assertEq(wsnr.totalSupply(), amount);
|
||||
assertEq(address(wsnr).balance, amount);
|
||||
}
|
||||
|
||||
function testFuzzWithdraw(uint256 depositAmount, uint256 withdrawAmount) public {
|
||||
vm.assume(depositAmount > 0 && depositAmount <= 100 ether);
|
||||
vm.assume(withdrawAmount > 0 && withdrawAmount <= depositAmount);
|
||||
|
||||
vm.deal(alice, depositAmount);
|
||||
vm.startPrank(alice);
|
||||
|
||||
wsnr.deposit{value: depositAmount}();
|
||||
wsnr.withdraw(withdrawAmount);
|
||||
|
||||
assertEq(wsnr.balanceOf(alice), depositAmount - withdrawAmount);
|
||||
assertEq(address(wsnr).balance, depositAmount - withdrawAmount);
|
||||
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testReentrancyProtection() public {
|
||||
ReentrantAttacker attacker = new ReentrantAttacker(wsnr);
|
||||
vm.deal(address(attacker), 10 ether);
|
||||
|
||||
// The reentrancy guard prevents the second withdraw, which causes
|
||||
// the ETH transfer to fail, resulting in "SNR transfer failed" error
|
||||
vm.expectRevert("WSNR: SNR transfer failed");
|
||||
attacker.attack{value: 2 ether}();
|
||||
}
|
||||
}
|
||||
|
||||
// Reentrancy test helper contract
|
||||
contract ReentrantAttacker {
|
||||
WSNR public wsnr;
|
||||
uint256 public attackCount;
|
||||
|
||||
constructor(WSNR _wsnr) {
|
||||
wsnr = _wsnr;
|
||||
}
|
||||
|
||||
receive() external payable {
|
||||
attackCount++;
|
||||
if (attackCount < 2 && address(wsnr).balance >= 1 ether) {
|
||||
wsnr.withdraw(1 ether);
|
||||
}
|
||||
}
|
||||
|
||||
function attack() external payable {
|
||||
wsnr.deposit{value: msg.value}();
|
||||
wsnr.withdraw(1 ether);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user