* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+15
View File
@@ -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=
+19
View File
@@ -0,0 +1,19 @@
# Foundry files
cache/
out/
broadcast/
# Environment
.env
.env.local
# Dependencies
lib/
# Coverage
lcov.info
coverage/
# OS
.DS_Store
.claude*
+153
View File
@@ -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
+105
View File
@@ -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)
+122
View File
@@ -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)
+34
View File
@@ -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!"
+25
View File
@@ -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
+54
View File
@@ -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;
}
}
+98
View File
@@ -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;
}
}
+53
View File
@@ -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;
}
+371
View File
@@ -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);
}
}