security
VeChain Smart Contract Security Checklist
When to use
Use when the user asks about: security, audit, vulnerability review, reentrancy, access control, or when reviewing contract code.
Core Principle
Assume the attacker controls:
- Every function argument
- Transaction ordering (front-running, sandwich attacks)
- External contract calls (reentrancy, composability exploits)
- Contract state between transactions
Vulnerability Categories
1. Reentrancy Attacks
Risk: External calls allow malicious contracts to re-enter your function before state updates complete.
Attack: Attacker's receive() or fallback() function calls back into the vulnerable contract, draining funds.
Prevention:
// Option 1: Use OpenZeppelin's ReentrancyGuard (recommended)
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
// Option 2: Checks-Effects-Interactions pattern
function withdraw(uint256 amount) external {
// Checks
require(balances[msg.sender] >= amount, "Insufficient");
// Effects (update state BEFORE external call)
balances[msg.sender] -= amount;
// Interactions (external call last)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
2. Access Control Vulnerabilities
Risk: Missing or incorrect access control allows unauthorized users to call privileged functions.
Attack: Attacker calls admin functions (mint, pause, upgrade, withdraw) directly.
Prevention:
// Use OpenZeppelin AccessControl or Ownable
import "@openzeppelin/contracts/access/Ownable.sol";
contract Secure is Ownable {
constructor() Ownable(msg.sender) {}
function adminAction() external onlyOwner {
// Only owner can call
}
}
// For granular roles:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBased is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
// Only accounts with MINTER_ROLE
}
}
Critical: Never use tx.origin for authorization:
// BAD: vulnerable to phishing via malicious contracts
require(tx.origin == owner, "Not owner");
// GOOD: use msg.sender
require(msg.sender == owner, "Not owner");
3. Integer Overflow/Underflow
Risk: Arithmetic operations wrap around, leading to unexpected values.
Note: Solidity 0.8+ has built-in overflow checks. However, unchecked blocks bypass these.
Prevention:
// Solidity 0.8+: safe by default
uint256 result = a + b; // Reverts on overflow
// When using unchecked, ensure overflow is impossible by construction
unchecked {
// ONLY when you've proven overflow cannot happen
uint256 i = 0;
i++; // Safe: bounded by loop condition
}
// Be careful with downcasting
uint256 bigValue = 300;
uint8 smallValue = uint8(bigValue); // Silently truncates to 44!
// Use SafeCast for safe downcasting
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
uint8 safeValue = SafeCast.toUint8(bigValue); // Reverts if > 255
4. Front-Running / MEV
Risk: Attackers observe pending transactions and submit their own with higher gas priority.
Attack: Sandwich attacks on DEX trades, front-running NFT mints, oracle manipulation.
Prevention:
// Use commit-reveal patterns for sensitive operations
mapping(bytes32 => uint256) public commitments;
function commit(bytes32 hash) external {
commitments[hash] = block.timestamp;
}
function reveal(uint256 value, bytes32 salt) external {
bytes32 hash = keccak256(abi.encodePacked(msg.sender, value, salt));
require(commitments[hash] > 0, "No commitment");
require(block.timestamp >= commitments[hash] + 10, "Too early"); // ~1 block on VeChain
delete commitments[hash];
// Process the revealed value
}
// Use slippage protection for swaps
function swap(uint256 amountIn, uint256 minAmountOut) external {
uint256 amountOut = calculateOutput(amountIn);
require(amountOut >= minAmountOut, "Slippage exceeded");
// Execute swap
}
Note: VeChain's ~10-second block time and different mempool dynamics make front-running less common than on Ethereum, but it is still possible.
5. Uninitialized Storage / Proxy Vulnerabilities
Risk: Upgradeable contracts can have uninitialized state or storage collisions.
Attack: Attacker calls initialize() on an uninitialized proxy or exploits storage layout conflicts.
Prevention:
// Always use initializers for upgradeable contracts
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyUpgradeable is Initializable {
uint256 public value;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // Prevent implementation initialization
}
function initialize(uint256 _value) public initializer {
value = _value;
}
}
- Never add new state variables between existing ones in upgrades
- Use OpenZeppelin's upgrade safety checks
- Always
_disableInitializers()in the constructor
6. Denial of Service (DoS)
Risk: Attacker makes a function unusable for legitimate users.
Attack: Gas griefing, unbounded loops, failed external calls blocking execution.
Prevention:
// BAD: Unbounded loop over user-controlled array
function distributeAll() external {
for (uint256 i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(amounts[i]); // DoS if one fails
}
}
// GOOD: Pull pattern (users withdraw themselves)
mapping(address => uint256) public pendingWithdrawals;
function withdraw() external {
uint256 amount = pendingWithdrawals[msg.sender];
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// GOOD: Batch with limit
function distributeBatch(uint256 start, uint256 count) external {
uint256 end = start + count;
require(end <= recipients.length, "Out of bounds");
for (uint256 i = start; i < end;) {
// process
unchecked { ++i; }
}
}
7. Oracle Manipulation
Risk: Price oracles or data feeds can be manipulated to exploit DeFi protocols.
Attack: Flash loan attack manipulates spot price, attacker profits from mispriced assets.
Prevention:
// Use time-weighted average prices (TWAP) instead of spot prices
// Use multiple oracle sources
// Add circuit breakers for extreme price movements
function getPrice() public view returns (uint256) {
uint256 price = oracle.getPrice();
require(price > minPrice && price < maxPrice, "Price out of bounds");
return price;
}
8. Unsafe External Calls
Risk: Low-level calls can fail silently or return unexpected data.
Prevention:
// BAD: ignoring return value
address(target).call{value: amount}("");
// GOOD: check return value
(bool success, bytes memory returnData) = address(target).call{value: amount}("");
require(success, "Call failed");
// BAD: using transfer (2300 gas limit, can break)
payable(recipient).transfer(amount);
// GOOD: use call with reentrancy protection
(bool success, ) = recipient.call{value: amount}("");
require(success, "Transfer failed");
// For ERC-20 tokens, use SafeERC20
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(recipient, amount);
token.safeTransferFrom(sender, recipient, amount);
9. Signature Replay
Risk: Valid signatures can be reused across transactions, chains, or contexts.
Prevention:
// Include nonce, chain ID, and contract address in signed data
mapping(address => uint256) public nonces;
function executeWithSignature(
address to,
uint256 amount,
uint256 nonce,
bytes calldata signature
) external {
require(nonce == nonces[msg.sender], "Invalid nonce");
nonces[msg.sender]++;
bytes32 hash = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR, // includes chain ID and contract address
keccak256(abi.encode(to, amount, nonce))
));
address signer = ECDSA.recover(hash, signature);
require(signer == msg.sender, "Invalid signature");
// Execute action
}
Smart Contract Checklist
Input Validation
- Validate all function parameters (non-zero addresses, valid ranges)
- Use
requireor custom errors for all preconditions - Validate array lengths match when processing parallel arrays
- Check for zero amounts in transfer/approval functions
Access Control
- Every privileged function has appropriate access control
- Use
Ownable2Stepfor ownership transfers - Never use
tx.originfor authorization - Admin functions are clearly identified and tested
Reentrancy
- Use
ReentrancyGuardon all functions that make external calls - Follow Checks-Effects-Interactions pattern
- State updates happen before external calls
Arithmetic
- Solidity 0.8+ is used (built-in overflow protection)
-
uncheckedblocks are only used where overflow is provably impossible - Safe downcasting with
SafeCastwhen needed - Division by zero is prevented
External Interactions
- Return values of external calls are checked
-
SafeERC20is used for token transfers - No reliance on
transfer()orsend()(usecallinstead) - Contract handles the case where external call reverts
Upgradeable Contracts
-
_disableInitializers()in constructor - Storage layout preserved across upgrades
-
initializermodifier on initialization functions - Upgrade authorization properly restricted
Client-Side Checklist
- Network awareness: never hardcode mainnet endpoints in dev flows
- Estimate gas before sending transactions
- Handle transaction confirmation properly (poll receipt)
- Treat "transaction ID received" as not-final; track confirmation
- Validate contract addresses against expected values
- Show clear error messages for revert reasons
- Handle fee delegation failures gracefully (fallback to user-paid)
- Never expose private keys in frontend code
VeChain-Specific Security Considerations
- Dual token model: Ensure contracts handle both VET and VTHO correctly
- Multi-clause atomicity: All clauses revert together; design accordingly
- Fee delegation: Validate that delegated transactions are properly authorized
- Block time: ~10 seconds; do not rely on sub-block-time precision for security
- EVM compatibility: Target
parisEVM version; newer opcodes will fail - Built-in contracts: Be aware of VeChainThor's genesis contracts and their interfaces
Security Review Questions
- Can an attacker re-enter any function via an external call?
- Can an attacker call privileged functions without authorization?
- Can an attacker manipulate arithmetic to gain an advantage?
- Can an attacker front-run a transaction to extract value?
- Can an attacker replay a valid signature in a different context?
- Can an attacker cause a DoS by making a function revert for everyone?
- Can an attacker exploit the upgrade mechanism?
- Can an attacker manipulate oracle data to misvalue assets?