Skip to main content

Overview

Security is paramount when building blockchain applications. This guide covers essential security practices for smart contract development on Paxeer Network.
Security is critical. A single vulnerability can lead to permanent loss of funds. Always prioritize security over speed of development.

Smart Contract Security

Use Audited Libraries

Always use well-tested, audited libraries like OpenZeppelin:
// ✅ Good: Use OpenZeppelin
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {}
}

// ❌ Bad: Roll your own implementation
contract MyToken {
    mapping(address => uint256) public balances;
    // Custom implementation may have bugs
}

OpenZeppelin Contracts

Industry-standard secure smart contract library

Prevent Reentrancy Attacks

Follow the CEI pattern:
function withdraw(uint256 amount) external {
    // ✅ CHECKS: Validate conditions first
    require(balances[msg.sender] >= amount, "Insufficient balance");
    require(amount > 0, "Amount must be > 0");
    
    // ✅ EFFECTS: Update state before external calls
    balances[msg.sender] -= amount;
    totalBalance -= amount;
    
    // ✅ INTERACTIONS: External calls last
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}
Why? If you make external calls before updating state, malicious contracts can re-enter and drain funds.

Access Control

Implement proper access control for sensitive functions:
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
    constructor() Ownable(msg.sender) {}
    
    function adminFunction() external onlyOwner {
        // Only owner can call
    }
    
    function transferOwnership(address newOwner) public override onlyOwner {
        // Transfer ownership safely
        super.transferOwnership(newOwner);
    }
}

Input Validation

Always validate inputs:
contract SecureContract {
    function transfer(address to, uint256 amount) external {
        // ✅ Validate address
        require(to != address(0), "Invalid address");
        require(to != address(this), "Cannot transfer to self");
        
        // ✅ Validate amount
        require(amount > 0, "Amount must be > 0");
        require(amount <= balances[msg.sender], "Insufficient balance");
        
        // ✅ Check for overflow (automatic in 0.8+)
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
    
    function processBatch(address[] calldata recipients, uint256[] calldata amounts) 
        external 
    {
        // ✅ Validate array lengths match
        require(recipients.length == amounts.length, "Length mismatch");
        
        // ✅ Validate array not too large
        require(recipients.length <= 100, "Batch too large");
        
        for (uint256 i = 0; i < recipients.length; i++) {
            require(recipients[i] != address(0), "Invalid recipient");
            require(amounts[i] > 0, "Invalid amount");
        }
    }
}

Integer Overflow Protection

Solidity 0.8+ has built-in overflow protection:
contract SafeMath {
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        // ✅ Automatically reverts on overflow in 0.8+
        return a + b;
    }
    
    function unsafeAdd(uint256 a, uint256 b) public pure returns (uint256) {
        // ❌ Can overflow - only use when certain it's safe
        unchecked {
            return a + b;
        }
    }
}
Only use unchecked blocks when you’re absolutely certain overflow is impossible, like incrementing a loop counter.

Safe External Calls

// ✅ Good: Handle failures
function sendETH(address payable to, uint256 amount) external {
    (bool success, ) = to.call{value: amount}("");
    require(success, "Transfer failed");
}

// ❌ Bad: Using transfer (can fail with gas limit)
function sendETHBad(address payable to, uint256 amount) external {
    to.transfer(amount); // Only 2300 gas forwarded
}

// ❌ Bad: Using send without checking
function sendETHWorse(address payable to, uint256 amount) external {
    to.send(amount); // Doesn't revert on failure!
}

Common Vulnerabilities

Risk Level: 🔴 CriticalExample Attack:
// Vulnerable contract
function withdraw() external {
    uint256 amount = balances[msg.sender];
    (bool success, ) = msg.sender.call{value: amount}(""); // ❌ External call first
    balances[msg.sender] = 0; // ❌ State update after
}
Protection:
  • Use ReentrancyGuard
  • Follow Checks-Effects-Interactions pattern
  • Update state before external calls
Risk Level: 🟡 MediumDescription: Attackers see pending transactions and submit their own with higher gas fees to execute first.Mitigation:
  • Use commit-reveal schemes
  • Implement minimum time delays
  • Use batch execution
  • Consider private mempools (Flashbots)
Risk Level: 🟢 Low (Solidity 0.8+)Protection:
  • Use Solidity 0.8+ (automatic protection)
  • Only use unchecked when absolutely safe
  • Test boundary conditions
Risk Level: 🟡 MediumExample:
// ❌ Bad: Ignoring return value
token.transfer(recipient, amount);

// ✅ Good: Checking return value
bool success = token.transfer(recipient, amount);
require(success, "Transfer failed");
Risk Level: 🔴 CriticalRisk: delegatecall executes code in context of callerProtection:
// ❌ Dangerous: User-controlled delegatecall
function execute(address target, bytes calldata data) external {
    target.delegatecall(data); // Very dangerous!
}

// ✅ Safe: Whitelist approved implementations
mapping(address => bool) public approvedImplementations;

function safeDelegatecall(address target, bytes calldata data) external {
    require(approvedImplementations[target], "Not approved");
    target.delegatecall(data);
}
Risk Level: 🔴 CriticalCommon Mistakes:
// ❌ Bad: tx.origin for auth
function withdraw() external {
    require(tx.origin == owner); // Can be bypassed!
}

// ✅ Good: msg.sender for auth
function withdraw() external {
    require(msg.sender == owner);
}

Security Tools

Security Checklist

Before deploying to mainnet:

Development Phase

  • Use latest Solidity version (0.8.20+)
  • Import audited libraries (OpenZeppelin)
  • Follow Checks-Effects-Interactions pattern
  • Implement access control
  • Validate all inputs
  • Use ReentrancyGuard for functions with external calls
  • Document all assumptions and invariants

Testing Phase

  • 90%+ test coverage
  • Test all edge cases
  • Fuzz test with Foundry/Echidna
  • Test failure scenarios
  • Test with maximum values
  • Test access control
  • Test reentrancy scenarios

Analysis Phase

  • Run Slither static analysis
  • Run Mythril security scanner
  • Manual code review
  • Check for common vulnerabilities
  • Review all external calls
  • Verify all math operations

Audit Phase

  • Internal security review
  • Peer review by team
  • External audit by professionals (for high-value contracts)
  • Bug bounty program (optional)

Deployment Phase

  • Deploy to testnet first
  • Test all functions on testnet
  • Verify contract on explorer
  • Set up monitoring
  • Prepare emergency response plan
  • Document all admin functions

Code Review Guidelines

Identify and scrutinize all external calls:
// Look for:
- .call()
- .delegatecall()
- .transfer()
- .send()
- External contract calls

// Ask:
- Can this reenter?
- Is the recipient trusted?
- Is the return value checked?
- Is state updated before the call?
Verify all privileged functions:
// Questions to ask:
- Who can call this function?
- Is there a modifier protecting it?
- Can ownership be renounced?
- Are there emergency controls?
- Is multi-sig recommended?
Analyze all state modifications:
// Check:
- Are balances updated correctly?
- Can state become inconsistent?
- Are events emitted for changes?
- Is data properly indexed?
Review arithmetic:
// Verify:
- No overflow/underflow (unless unchecked)
- Division by zero protection
- Rounding handled correctly
- Precision loss considered

Emergency Procedures

Pause Mechanism

Implement emergency pause:
import "@openzeppelin/contracts/security/Pausable.sol";

contract EmergencyPausable is Pausable, Ownable {
    constructor() Ownable(msg.sender) {}
    
    function pause() external onlyOwner {
        _pause();
    }
    
    function unpause() external onlyOwner {
        _unpause();
    }
    
    function transfer(address to, uint256 amount) 
        external 
        whenNotPaused 
    {
        // Function pauses in emergency
    }
}

Upgrade Patterns

// Use OpenZeppelin's upgradeable contracts
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyTokenUpgradeable is 
    Initializable, 
    ERC20Upgradeable, 
    OwnableUpgradeable 
{
    function initialize() public initializer {
        __ERC20_init("MyToken", "MTK");
        __Ownable_init(msg.sender);
    }
}

Testing for Security

Security-Focused Tests

test/Security.test.js
describe("Security Tests", function () {
  it("Should prevent reentrancy", async function () {
    const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
    const attacker = await Attacker.deploy(contract.address);
    
    await expect(
      attacker.attack()
    ).to.be.revertedWith("ReentrancyGuard: reentrant call");
  });

  it("Should prevent unauthorized access", async function () {
    await expect(
      contract.connect(user).adminFunction()
    ).to.be.revertedWith("Ownable: caller is not the owner");
  });

  it("Should handle zero address", async function () {
    await expect(
      contract.transfer(ethers.ZeroAddress, 100)
    ).to.be.revertedWith("Invalid address");
  });

  it("Should prevent integer overflow", async function () {
    const max = ethers.MaxUint256;
    await expect(
      contract.unsafeAdd(max, 1)
    ).to.be.reverted;
  });
});

Invariant Testing (Foundry)

test/Invariants.t.sol
contract InvariantTest is Test {
    MyContract contract;
    
    function setUp() public {
        contract = new MyContract();
    }
    
    // Total supply should never change
    function invariant_totalSupply() public {
        assertEq(
            contract.totalSupply(),
            INITIAL_SUPPLY
        );
    }
    
    // Balance sum should equal total supply
    function invariant_balanceSum() public {
        uint256 sum = 0;
        address[] memory users = contract.getUsers();
        
        for (uint i = 0; i < users.length; i++) {
            sum += contract.balanceOf(users[i]);
        }
        
        assertEq(sum, contract.totalSupply());
    }
}

Audit Preparation

Documentation

Create comprehensive documentation:
# Contract Documentation

## Overview
[What does this contract do?]

## Architecture
[How is it structured?]

## Functions
### Public Functions
- `function1()`: [Description, parameters, return values]
- `function2()`: [Description, parameters, return values]

### Admin Functions
- `adminFunc()`: [Description, access control, risks]

## Security Considerations
[Known risks and mitigations]

## Dependencies
[External contracts and libraries]

## Assumptions
[What assumptions does the code make?]

Prepare Audit Package

audit-package/
├── contracts/          # All contract source files
├── test/              # Comprehensive test suite
├── docs/              # Documentation
│   ├── architecture.md
│   ├── security.md
│   └── functions.md
├── audit-scope.md     # What to audit
└── known-issues.md    # Known limitations

Getting Audited

Bug Bounty Programs

Consider running a bug bounty:
# Bug Bounty Program

## Rewards
- Critical: $10,000 - $50,000
- High: $5,000 - $10,000
- Medium: $1,000 - $5,000
- Low: $100 - $1,000

## Scope
- All smart contracts at [addresses]
- Vulnerabilities that lead to loss of funds
- Access control bypasses
- Economic attacks

## Out of Scope
- Known issues
- Gas optimizations
- UI/UX issues

## Reporting
Submit to [email protected]

Resources

Next Steps