Skip to main content

Part 4: Gas Optimization & Deployment

๐ŸŽฏ Tujuanโ€‹

Setelah menyelesaikan bagian ini, Anda akan:

  • โœ… Memahami dan menerapkan gas optimization techniques
  • โœ… Mengukur gas savings dengan snapshots
  • โœ… Men-deploy EduLoan ke Mantle Sepolia
  • โœ… Memverifikasi contract di explorer
  • โœ… Berinteraksi dengan deployed contract
  • โœ… Menggunakan production deployment checklist

๐Ÿ’ก Kenapa Gas Optimization Penting?โ€‹

Analogi: Bensin Mobilโ€‹

Mobil Boros vs Irit:

Jakarta โ†’ Bandung (150 km)

Mobil boros (20L/100km):
Bensin: 30L x Rp 15,000 = Rp 450,000 ๐Ÿ’ธ

Mobil irit (8L/100km):
Bensin: 12L x Rp 15,000 = Rp 180,000 โœ…

Hemat: Rp 270,000 per trip!

Smart Contract:

Function call (tanpa optimization):
Gas: 200,000 x 50 gwei = 0.01 ETH
Dengan ETH $3000 = $30 per call ๐Ÿ’ธ

Function call (dengan optimization):
Gas: 50,000 x 50 gwei = 0.0025 ETH
Dengan ETH $3000 = $7.50 per call โœ…

Hemat: $22.50 per call!
1000 users = $22,500 saved!

Mantle Network Advantageโ€‹

Gas Comparison:

Ethereum Mainnet:
Deploy EduLoan: ~$150-300
Function call: ~$10-50

Mantle Sepolia (Testnet):
Deploy EduLoan: FREE (testnet)
Function call: FREE (testnet)

Mantle Mainnet:
Deploy EduLoan: ~$1-5
Function call: ~$0.01-0.10

Savings: 95%+ cheaper than Ethereum! โšก

๐Ÿ“Š Measure Current Gas Usageโ€‹

Run Gas Reportโ€‹

forge test --gas-report

Output (before optimization):

| src/EduLoan.sol:EduLoan contract |                 |        |        |        |         |
|----------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1234567 | 5678 | | | | |
| Function Name | min | avg | median | max | # calls |
| applyLoan | 145678 | 156789 | 156789 | 167890 | 15 |
| approveLoan | 34567 | 45678 | 45678 | 56789 | 10 |
| disburseLoan | 67890 | 78901 | 78901 | 89012 | 8 |
| makePayment | 45678 | 56789 | 56789 | 67890 | 12 |
| calculateInterest | 345 | 345 | 345 | 345 | 5 |

Create Gas Snapshotโ€‹

forge snapshot

File .gas-snapshot created:

EduLoanTest:test_ApplyLoan() (gas: 156789)
EduLoanTest:test_ApproveLoan() (gas: 112345)
EduLoanTest:test_DisburseLoan() (gas: 189012)
EduLoanTest:test_MakePayment() (gas: 145678)
...

โšก Gas Optimization Techniquesโ€‹

Technique 1: Use Custom Errors (Already Done!)โ€‹

โŒ Expensive:

require(msg.sender == admin, "Hanya admin!");
// String stored on-chain = expensive!
// ~2400 gas

โœ… Cheap (what we have):

// Kita sudah pakai require dengan string pendek
// Tapi bisa lebih murah dengan custom errors:
error NotAdmin();

if (msg.sender != admin) revert NotAdmin();
// ~200 gas

Savings: ~2200 gas per check (91% cheaper!)

Technique 2: Cache Storage Variablesโ€‹

โŒ Before (multiple storage reads):

function makePayment(uint256 _loanId) public payable {
require(loans[_loanId].status == LoanStatus.Active);
loans[_loanId].amountRepaid += msg.value;
if (loans[_loanId].amountRepaid >= loans[_loanId].totalAmount) {
loans[_loanId].status = LoanStatus.Repaid;
}
// loans[_loanId] dibaca 4 kali = 4 x SLOAD (2100 gas each)
}

โœ… After (cache in memory):

function makePayment(uint256 _loanId) public payable {
Loan storage loan = loans[_loanId]; // 1x SLOAD
require(loan.status == LoanStatus.Active);
loan.amountRepaid += msg.value;
if (loan.amountRepaid >= loan.totalAmount) {
loan.status = LoanStatus.Repaid;
}
// Hanya 1x SLOAD, sisanya memory reads
}

Savings: ~6300 gas (75% cheaper for reads!)

Technique 3: Use external Instead of publicโ€‹

โŒ Less efficient:

function applyLoan(uint256 _amount, string memory _purpose) public {
// 'public' copies parameters to memory
}

โœ… More efficient:

function applyLoan(uint256 _amount, string calldata _purpose) external {
// 'external' + 'calldata' = no copy, direct read
}

Savings: ~200-1000 gas per call

Technique 4: Use unchecked for Safe Mathโ€‹

โŒ Default (with overflow checks):

loan.amountRepaid += msg.value;
// Solidity 0.8+ adds overflow check
// Extra ~30 gas per operation

โœ… Optimized (when safe):

unchecked {
loan.amountRepaid += msg.value;
// No overflow check - we know it's safe because
// amountRepaid can't exceed totalAmount realistically
}

Savings: ~30 gas per math operation

Technique 5: Short-Circuit Conditionsโ€‹

โŒ Expensive first:

if (loans[_loanId].status == LoanStatus.Active && amount > 0) {
// Storage read happens first, even if amount = 0
}

โœ… Cheap first:

if (amount > 0 && loans[_loanId].status == LoanStatus.Active) {
// If amount = 0, storage read skipped!
}

๐Ÿ”„ Create Optimized EduLoanโ€‹

EduLoanOptimized.solโ€‹

Buat src/EduLoanOptimized.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

/// @title EduLoanOptimized - Gas-Optimized Version
/// @notice Sistem pinjaman dengan gas optimization

contract EduLoanOptimized {
// ============================================
// CUSTOM ERRORS (Gas Efficient!)
// ============================================

error NotAdmin();
error NotBorrower();
error LoanNotFound();
error InvalidStatus();
error InvalidAmount();
error InsufficientBalance();
error TransferFailed();

// ============================================
// ENUMS & STRUCTS
// ============================================

enum LoanStatus {
Pending,
Approved,
Active,
Repaid,
Defaulted,
Rejected
}

struct Loan {
uint256 loanId;
address borrower;
uint256 principalAmount;
uint256 interestRate;
uint256 totalAmount;
uint256 amountRepaid;
uint256 applicationTime;
uint256 approvalTime;
uint256 deadline;
LoanStatus status;
string purpose;
}

// ============================================
// STATE VARIABLES
// ============================================

address public admin;
uint256 public loanCounter;

uint256 public constant INTEREST_RATE = 500;
uint256 public constant LOAN_DURATION = 365 days;
uint256 public constant MIN_LOAN = 0.01 ether;
uint256 public constant MAX_LOAN = 10 ether;

mapping(uint256 => Loan) public loans;
mapping(address => uint256[]) public borrowerLoans;

// ============================================
// EVENTS
// ============================================

event LoanApplied(uint256 indexed loanId, address indexed borrower, uint256 amount, string purpose);
event LoanApproved(uint256 indexed loanId, address indexed borrower, uint256 totalAmount);
event LoanRejected(uint256 indexed loanId, address indexed borrower, string reason);
event LoanDisbursed(uint256 indexed loanId, address indexed borrower, uint256 amount);
event PaymentMade(uint256 indexed loanId, address indexed borrower, uint256 amount, uint256 remaining);
event LoanRepaid(uint256 indexed loanId, address indexed borrower);
event LoanDefaulted(uint256 indexed loanId, address indexed borrower);
event FundsDeposited(address indexed admin, uint256 amount);
event FundsWithdrawn(address indexed admin, uint256 amount);

// ============================================
// MODIFIERS (Gas Optimized)
// ============================================

modifier onlyAdmin() {
if (msg.sender != admin) revert NotAdmin();
_;
}

modifier onlyBorrower(uint256 _loanId) {
if (loans[_loanId].borrower != msg.sender) revert NotBorrower();
_;
}

modifier loanExists(uint256 _loanId) {
if (_loanId == 0 || _loanId > loanCounter) revert LoanNotFound();
_;
}

modifier inStatus(uint256 _loanId, LoanStatus _status) {
if (loans[_loanId].status != _status) revert InvalidStatus();
_;
}

// ============================================
// CONSTRUCTOR
// ============================================

constructor() {
admin = msg.sender;
}

// ============================================
// MAIN FUNCTIONS (Optimized)
// ============================================

/// @notice Mahasiswa mengajukan pinjaman
function applyLoan(uint256 _amount, string calldata _purpose) external {
// Cheap checks first
if (_amount < MIN_LOAN) revert InvalidAmount();
if (_amount > MAX_LOAN) revert InvalidAmount();

// Increment counter
uint256 newLoanId;
unchecked {
newLoanId = ++loanCounter;
}

// Calculate interest
uint256 interest = calculateInterest(_amount);
uint256 total;
unchecked {
total = _amount + interest;
}

// Create loan
loans[newLoanId] = Loan({
loanId: newLoanId,
borrower: msg.sender,
principalAmount: _amount,
interestRate: INTEREST_RATE,
totalAmount: total,
amountRepaid: 0,
applicationTime: block.timestamp,
approvalTime: 0,
deadline: 0,
status: LoanStatus.Pending,
purpose: _purpose
});

borrowerLoans[msg.sender].push(newLoanId);

emit LoanApplied(newLoanId, msg.sender, _amount, _purpose);
}

/// @notice Admin menyetujui pinjaman
function approveLoan(uint256 _loanId)
external
onlyAdmin
loanExists(_loanId)
inStatus(_loanId, LoanStatus.Pending)
{
Loan storage loan = loans[_loanId]; // Cache storage pointer
loan.status = LoanStatus.Approved;
loan.approvalTime = block.timestamp;

emit LoanApproved(_loanId, loan.borrower, loan.totalAmount);
}

/// @notice Admin menolak pinjaman
function rejectLoan(uint256 _loanId, string calldata _reason)
external
onlyAdmin
loanExists(_loanId)
inStatus(_loanId, LoanStatus.Pending)
{
Loan storage loan = loans[_loanId];
loan.status = LoanStatus.Rejected;

emit LoanRejected(_loanId, loan.borrower, _reason);
}

/// @notice Admin mencairkan dana
function disburseLoan(uint256 _loanId)
external
onlyAdmin
loanExists(_loanId)
inStatus(_loanId, LoanStatus.Approved)
{
Loan storage loan = loans[_loanId];
uint256 amount = loan.principalAmount;

if (address(this).balance < amount) revert InsufficientBalance();

unchecked {
loan.deadline = block.timestamp + LOAN_DURATION;
}
loan.status = LoanStatus.Active;

(bool success, ) = loan.borrower.call{value: amount}("");
if (!success) revert TransferFailed();

emit LoanDisbursed(_loanId, loan.borrower, amount);
}

/// @notice Borrower membayar cicilan
function makePayment(uint256 _loanId)
external
payable
loanExists(_loanId)
onlyBorrower(_loanId)
inStatus(_loanId, LoanStatus.Active)
{
if (msg.value == 0) revert InvalidAmount();

Loan storage loan = loans[_loanId];

unchecked {
loan.amountRepaid += msg.value;
}

uint256 remaining;
if (loan.totalAmount > loan.amountRepaid) {
unchecked {
remaining = loan.totalAmount - loan.amountRepaid;
}
}

if (loan.amountRepaid >= loan.totalAmount) {
loan.status = LoanStatus.Repaid;
emit LoanRepaid(_loanId, msg.sender);
}

emit PaymentMade(_loanId, msg.sender, msg.value, remaining);
}

/// @notice Cek default
function checkDefault(uint256 _loanId) external loanExists(_loanId) {
Loan storage loan = loans[_loanId];

if (loan.status != LoanStatus.Active) revert InvalidStatus();

if (block.timestamp > loan.deadline && loan.amountRepaid < loan.totalAmount) {
loan.status = LoanStatus.Defaulted;
emit LoanDefaulted(_loanId, loan.borrower);
}
}

// ============================================
// VIEW FUNCTIONS
// ============================================

function getLoanDetails(uint256 _loanId)
external
view
loanExists(_loanId)
returns (Loan memory)
{
return loans[_loanId];
}

function getMyLoans() external view returns (uint256[] memory) {
return borrowerLoans[msg.sender];
}

function calculateInterest(uint256 _principal) public pure returns (uint256) {
return (_principal * INTEREST_RATE) / 10000;
}

function getRemainingAmount(uint256 _loanId)
external
view
loanExists(_loanId)
returns (uint256)
{
Loan memory loan = loans[_loanId];
if (loan.amountRepaid >= loan.totalAmount) return 0;
unchecked {
return loan.totalAmount - loan.amountRepaid;
}
}

function getContractBalance() external view returns (uint256) {
return address(this).balance;
}

function getTotalLoans() external view returns (uint256) {
return loanCounter;
}

// ============================================
// ADMIN FUNCTIONS
// ============================================

function depositFunds() external payable onlyAdmin {
if (msg.value == 0) revert InvalidAmount();
emit FundsDeposited(msg.sender, msg.value);
}

function withdrawFunds(uint256 _amount) external onlyAdmin {
if (_amount == 0) revert InvalidAmount();
if (address(this).balance < _amount) revert InsufficientBalance();

(bool success, ) = admin.call{value: _amount}("");
if (!success) revert TransferFailed();

emit FundsWithdrawn(msg.sender, _amount);
}

function transferAdmin(address _newAdmin) external onlyAdmin {
if (_newAdmin == address(0)) revert InvalidAmount();
admin = _newAdmin;
}

receive() external payable {
emit FundsDeposited(msg.sender, msg.value);
}
}

๐Ÿ“Š Compare Gas Usageโ€‹

Run Comparisonโ€‹

# Original
forge test --match-contract EduLoanTest --gas-report

# Optimized (create test for optimized version)
forge test --match-contract EduLoanOptimizedTest --gas-report

Expected Savingsโ€‹

FunctionOriginalOptimizedSavings
applyLoan156,789142,345~9%
approveLoan45,67838,901~15%
disburseLoan78,90168,234~14%
makePayment56,78948,123~15%
Modifiers~2,400 each~200 each91%

Snapshot Comparisonโ€‹

# Create baseline
forge snapshot --snap baseline.snap

# After optimization
forge snapshot --diff baseline.snap

Output:

test_ApplyLoan() (gas: -14444 โฌ‡๏ธ 9.2%)
test_ApproveLoan() (gas: -6777 โฌ‡๏ธ 14.8%)
test_MakePayment() (gas: -8666 โฌ‡๏ธ 15.3%)

๐Ÿš€ Deploy to Mantle Sepoliaโ€‹

Step 1: Setup Environmentโ€‹

Buat file .env:

# Private Key (TANPA 0x prefix)
PRIVATE_KEY=your_private_key_here

# RPC URL
MANTLE_SEPOLIA_RPC=https://rpc.sepolia.mantle.xyz

# Explorer API (optional for Mantle)
ETHERSCAN_API_KEY=your_api_key

โš ๏ธ SECURITY:

  • Jangan commit .env ke git!
  • Gunakan wallet testnet saja!

Step 2: Create Deploy Scriptโ€‹

Buat script/DeployEduLoan.s.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Script, console} from "forge-std/Script.sol";
import {EduLoan} from "../src/EduLoan.sol";

contract DeployEduLoan is Script {
function run() external returns (EduLoan) {
// Load private key
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

// Start broadcasting
vm.startBroadcast(deployerPrivateKey);

// Deploy
EduLoan eduLoan = new EduLoan();

console.log("EduLoan deployed to:", address(eduLoan));
console.log("Admin:", eduLoan.admin());

vm.stopBroadcast();

return eduLoan;
}
}

Step 3: Load Environmentโ€‹

source .env

Step 4: Deployโ€‹

Dry Run (Simulation):

forge script script/DeployEduLoan.s.sol --rpc-url $MANTLE_SEPOLIA_RPC

Deploy for Real:

forge script script/DeployEduLoan.s.sol \
--rpc-url $MANTLE_SEPOLIA_RPC \
--broadcast \
-vvvv

Expected Output:

[โ Š] Compiling...
No files changed, compilation skipped

Script ran successfully.

== Logs ==
EduLoan deployed to: 0x1234567890abcdef1234567890abcdef12345678
Admin: 0xYourWalletAddress

## Setting up 1 EVM.
==========================
Chain 5003
Estimated gas price: 0.05 gwei
Estimated total gas used for script: 1234567
Estimated amount required: 0.0000617283 ETH
==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

Transactions saved to: broadcast/DeployEduLoan.s.sol/5003/run-latest.json

Step 5: Save Contract Addressโ€‹

# Contract address dari output
export EDULOAN_ADDRESS=0x1234567890abcdef1234567890abcdef12345678

๐Ÿ” Verify Contractโ€‹

Method 1: Forge Verify (If Explorer Supports)โ€‹

forge verify-contract $EDULOAN_ADDRESS \
src/EduLoan.sol:EduLoan \
--chain 5003 \
--watch

Method 2: Manual Verificationโ€‹

  1. Buka https://sepolia.mantlescan.xyz
  2. Search contract address
  3. Klik "Contract" tab
  4. Klik "Verify and Publish"
  5. Pilih:
    • Compiler Type: Solidity (Single file)
    • Compiler Version: v0.8.30
    • License: MIT
  6. Paste source code
  7. Submit

๐Ÿงช Test Deployed Contractโ€‹

Using Castโ€‹

1. Check Admin:

cast call $EDULOAN_ADDRESS "admin()" --rpc-url $MANTLE_SEPOLIA_RPC

2. Check Balance:

cast call $EDULOAN_ADDRESS "getContractBalance()" --rpc-url $MANTLE_SEPOLIA_RPC

3. Deposit Funds (Admin):

cast send $EDULOAN_ADDRESS "depositFunds()" \
--value 1ether \
--private-key $PRIVATE_KEY \
--rpc-url $MANTLE_SEPOLIA_RPC

4. Apply Loan:

cast send $EDULOAN_ADDRESS \
"applyLoan(uint256,string)" 10000000000000000 "SPP" \
--private-key $PRIVATE_KEY \
--rpc-url $MANTLE_SEPOLIA_RPC

5. Check Loan Details:

cast call $EDULOAN_ADDRESS "getLoanDetails(uint256)" 1 \
--rpc-url $MANTLE_SEPOLIA_RPC

โœ… Production Deployment Checklistโ€‹

Pre-Deploymentโ€‹

Code Quality:

  • All tests passing (forge test)
  • 100% test coverage (forge coverage)
  • No compiler warnings
  • Code reviewed

Gas Optimization:

  • Gas report reviewed
  • Optimizations applied
  • Contract size < 24KB

Security:

  • Access control verified
  • Reentrancy protection (CEI pattern)
  • Input validation complete
  • Custom errors implemented

Deploymentโ€‹

Environment:

  • Private key secured
  • RPC endpoint working
  • Enough MNT for gas
  • Testnet deployment successful

Process:

  • Dry run successful
  • Deploy transaction confirmed
  • Contract address saved
  • Verification submitted

Post-Deploymentโ€‹

Verification:

  • Contract verified on explorer
  • Source code matches
  • Admin address correct

Testing:

  • depositFunds() working
  • applyLoan() working
  • approveLoan() working
  • Full flow tested

๐Ÿ“š Command Referenceโ€‹

# === Build ===
forge build # Compile
forge build --sizes # With sizes

# === Test ===
forge test # Run tests
forge test --gas-report # With gas
forge coverage # Coverage

# === Gas Analysis ===
forge snapshot # Create snapshot
forge snapshot --diff <file> # Compare

# === Deploy ===
forge script <script> --rpc-url <url> # Dry run
forge script <script> --rpc-url <url> --broadcast # Deploy

# === Verify ===
forge verify-contract <address> <contract> --chain <id>

# === Interact ===
cast call <address> "function()" --rpc-url <url> # Read
cast send <address> "function()" --rpc-url <url> # Write

๐ŸŽ“ Summaryโ€‹

Apa yang sudah dipelajari:

โœ… Gas Optimization:

  • Custom errors vs require strings (91% savings)
  • Storage caching
  • external vs public
  • unchecked math
  • Short-circuit conditions

โœ… Deployment:

  • Deployment scripts di Solidity
  • Deploy ke Mantle Sepolia
  • Environment variables
  • Contract verification

โœ… Production Workflow:

  • Pre-deployment checklist
  • Testing requirements
  • Post-deployment verification

EduLoan Contract:

โœ… Fully tested (100% coverage)
โœ… Gas optimized
โœ… Deployed to Mantle Sepolia
โœ… Verified on explorer
โœ… Production ready!

๐Ÿ† Congratulations!โ€‹

Anda telah menyelesaikan Foundry Workshop untuk EduLoan! ๐ŸŽ‰

Skills yang Anda kuasai:

  • โš’๏ธ Foundry suite (forge, cast, anvil, chisel)
  • ๐Ÿงช Comprehensive testing dalam Solidity
  • ๐ŸŽฒ Fuzz testing untuk edge cases
  • โšก Gas optimization techniques
  • ๐Ÿš€ Professional deployment workflow

You're now ready to:

  • Build production-grade smart contracts
  • Contribute to DeFi protocols
  • Participate in audit contests
  • Deploy confidently to mainnet

๐Ÿ”— Resourcesโ€‹


โ† Back to Workshop Overview


#BuildWithFoundry | #MantleNetwork | #EthereumJakarta