So, you want to write a smart contract. Great! Youâve got dreams of decentralization, passive income, and maybe even a token named after your cat. But before you deploy and accidentally mint 2 billion extra tokens or lose everything in a reentrancy blackhole⊠sit down. Breathe.
Ah yes, the good old days (pre-Solidity 0.8.0) when subtracting 1 from 0 gave you 2â”â¶-1 and nobody blinked.
Rookie mistake:
// Solidity <0.8.0
uint8 balance = 0;
balance -= 1; // Surprise! balance = 255
Use SafeMath, unless you're into heart attacks:
// Solidity >=0.8.0 has built-in checks. But let's pretend it's 2019 again:
using SafeMath for uint256;
uint256 balance = 0;
balance = balance.sub(1); // Revert with "SafeMath: subtraction overflow"
What it feels like debugging overflow bugs:
"Why does this token supply say 1.1 quattuordecillion?"
Say youâre building a bank contract. (Bold move. Auditors are already sweating.)
Hereâs how NOT to do it:
Editfunction withdraw() public {
require(balances[msg.sender] > 0);
(bool sent, ) = msg.sender.call{value: balances[msg.sender]}(""); // đŹ External call first?
require(sent, "Failed to send Ether");
balances[msg.sender] = 0; // Too late, bro.
}
Attackerâs dream:
Editfallback() external payable {
victim.withdraw(); // Yo-yo time!
}
How to fix it (and keep your ETH):
// Check-Effects-Interactions pattern
function withdraw() public nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0);
balances[msg.sender] = 0; // Update balance first
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");
}
Bonus tip:
"@openzeppelin/contracts/security/ReentrancyGuard.sol";
Be smart. Be guarded. Like a smart contract Kevin Costner.
You think youâre clever. You write:
function storeData() public {
MyStruct storage s; // Uh-oh.
s.value = 42;
}
This doesnât "create" a new struct. This says: "Hey, modify random storage slot zero!"
Result: You may overwrite important contract data like the owner address, or your dignity.
Do this instead:
MyStruct storage s = myStructs[msg.sender]; // Actual location
Imagine this:
function sendRewards() public {
for (uint i = 0; i < users.length; i++) {
users[i].transfer(1 ether); // expensive AF
}
}
User list gets long. Function dies of gas exhaustion. R.I.P.
Fix:
Use pull over push
method.
Limit batch size.
Add checkpointing logic.
Or just donât build Ethereum into a to-do list app.
ExternalService external = ExternalService(externalAddress);
external.doSomethingCritical(); // Checks? Nah. YOLO.
This is how rug pulls happen.
Better:
(bool success, bytes memory data) = externalAddress.call(abi.encodeWithSignature("doSomethingCritical()"));
require(success, "External call failed");
if (abi.decode(data, (bool)) != true) {
revert("That external contract is sus");
}
Or use interfaces and only call verified contracts. You wouldn't give your wallet to a guy in an alley with "TotallyNotAScam.eth" on his jacket, right?
Smart contracts are like vending machines: automatic, immutable, and occasionally prone to spitting out flaming toads when you mess up.
So, use require
, modifier
, and humility
liberally. Always test like your ETH depends on it â because it does.
Problem | Fix |
---|---|
Overflow/Underflow | Use SafeMath or Solidity â„ 0.8.0 |
Reentrancy | Checks-Effects-Interactions / |
Uninitialized Pointers | Always assign storage vars explicitly |
DoS via gas | Avoid unbounded loops / Use batching |
Unchecked Calls | Validate return values / Use trusted interfaces |
May your gas be cheap and your audits thorough.
Deploy responsibly, you must.
Fabian Owuor