Solidity: Multicall - Aggregate Multiple Contract Calls

Solidity: Multicall - Aggregate Multiple Contract Calls

There are different implementations of multicall:

In the following section, we will use Multicaller as an example to illustrate the process.

The main idea of Multicaller is to aggregate multiple contract function calls into a single one. It's usually to batch contract reads from off-chain apps. However, it could also be used to batch contract writes.

Multiple Contract Reads

import { defaultAbiCoder } from "ethers/lib/utils"

class Liquidator {
    async fetchIsLiquidatableResults(
        marketId: number,
        positions: Position[],
    ) {
        const price = await this.pythService.fetchPythOraclePrice(marketId)

        const targets = new Array(positions.length).fill(this.exchange.address)
        const data = positions.map(position =>
            this.exchange.interface.encodeFunctionData("isLiquidatable", [
                marketId,
                position.account,
                price,
            ]),
        )
        const values = new Array(accountMarkets.length).fill(0)

        return await this.multicaller.callStatic.aggregate(targets, data, values)
    }

    async start() {
        const positions = await this.fetchPositions(marketId)
        const results = await this.fetchIsLiquidatableResults(marketId, positions)

        for (const [i, result] of results.entries()) {
            const isLiquidatable = defaultAbiCoder.decode(["bool"], result)[0]
            const position = positions[i]
            console.log(`${position.account} isLiquidatable: ${isLiquidatable}`)
        }
    }
}

ref:
https://github.com/Vectorized/multicaller/blob/main/API.md#aggregate

Multiple Contract Writes

It requires the target contract is compatible with Multicaller if the target contract needs to read msg.sender.

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import { LibMulticaller } from "multicaller/LibMulticaller.sol";

contract MulticallerSenderCompatible {
    function _sender() internal view virtual returns (address) {
        return LibMulticaller.sender();
    }
}

contract Exchange is MulticallerSenderCompatible {
    function openPosition(OpenPositionParams calldata params) external returns (int256, int256) {
        address taker = _sender();
        return _openPositionFor(taker, params);
    }
}
class Bot {
    async openPosition() {
        const targets = [
            this.oracleAdapter.address,
            this.exchange.address,
        ]
        const data = [
            this.oracleAdapter.interface.encodeFunctionData("updatePrice", [priceId, priceData]),
            this.exchange.interface.encodeFunctionData("openPosition", [params]),
        ]
        const values = [
            BigNumber.from(0),
            BigNumber.from(0),
        ]

        // update oracle price first, then open position
        const tx = await this.multicaller.connect(taker).aggregateWithSender(targets, data, values)
        await tx.wait()
    }
}

ref:
https://github.com/Vectorized/multicaller/blob/main/API.md#aggregatewithsender

Demystifying Solidity ABI Encoding

Demystifying Solidity ABI Encoding

There are some encode/decode functions in Solidity, for instance:

  • <code>abi.encode()</code> will concatenate all values and add padding to fit into 32 bytes for each values.
    • To integrate with other contracts, you should use <code>abi.encode()</code>.
  • <code>abi.encodePacked()</code> will concatenate all values in the exact byte representations without padding.
    • If you only need to store it, you should use <code>abi.encodePacked()</code> since it's smaller.
  • <code>abi.encodeWithSignature()</code> is mainly used to call functions in another contract.
  • <code>abi.encodeCall()</code> is the type-safe version of <code>abi.encodeWithSignature()</code>, required 0.8.11+.
pragma solidity >=0.8.19;

import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "forge-std/Test.sol";

contract MyTest is Test {
    function test_abi_encode() public {
        bytes memory result = abi.encode(uint8(1), uint16(2), uint24(3));
        console.logBytes(result);
        // 0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
        // total 32 bytes * 3 = 96 bytes
    }

    function test_abi_encodePacked() public {
        bytes memory resultPacked = abi.encodePacked(uint8(1), uint16(2), uint24(3));
        console.logBytes(resultPacked);
        // 0x010002000003
        // total 1 byte + 2 bytes + 3 bytes = 6 bytes
    }

    function test_abi_encodeWithSignature() public {
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        address vitalik = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
        bytes memory data = abi.encodeWithSignature("balanceOf(address)", vitalik);
        console.logBytes(data);
        (bool success, bytes memory result) = weth.call(data);
        console.logBool(success);
        console.logUint(abi.decode(result, (uint256)));
    }

    function test_abi_encodeCall() public {
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        address vitalik = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
        bytes memory data = abi.encodeCall(IERC20.balanceOf, (vitalik));
        console.logBytes(data);
        (bool success, bytes memory result) = weth.call(data);
        console.logBool(success);
        console.logUint(abi.decode(result, (uint256)));
    }
}
forge test --mc "MyTest" -vv --fork-url https://rpc.flashbots.net

ref:
https://github.com/AmazingAng/WTF-Solidity/tree/main/27_ABIEncode
https://trustchain.medium.com/abi-functions-explained-in-solidity-bd93cf88bdf2

Solidity: Read Contract Storage by Slots with Foundry

Solidity: Read Contract Storage by Slots with Foundry

State variables are stored in different "slots" in a contract, and each slot is 32 bytes (256 bits). However, multiple adjacent state variables declared in the contract may be packed into the same slot by the Solidity compiler if their combined size does not exceed 32 bytes.

When it comes to mappings and dynamic arrays, things get complicated. As an example, consider the following contract:

contract MyContract {
    struct Market {
        uint256 marketId; // slot: key's slot + 0
        bytes32 priceFeedId; // slot: key's slot + 1
    }

    address public owner; // slot 0
    uint256 public counter; // slot 1
    mapping(uint256 => Market) public marketMap; // slot 2

    constructor() {
        owner = msg.sender;
    }

    function createMarket(uint256 marketId, bytes32 priceFeedId) external {
        marketMap[marketId] = Market(marketId, priceFeedId);
    }

    function getMarket(uint256 marketId) external view returns (Market memory) {
        return marketMap[marketId];
    }
}

The elements (values) of a mapping are stored in different storage slots that are computed using keccak256() with the key and the slot number of that mapping. Two arbitrary keys' slots are very unlikely to be adjacent since the slot number is derived from a hash function. Nevertheless, the slots of members within a struct are laid out sequentially, marketId and priceFeedId in the above case, so they will be adjacent.

You could use vm.load() or stdstore.read() to read values by slots directly in Foundry. Though the later method requires the state variable to be public.

import "forge-std/Test.sol";

contract MyTest is Test {
    using stdStorage for StdStorage;

    MyContract myContract;
    uint256 ownerSlot = 0;
    uint256 mappingSlot = 2;

    MyContract.Market market;
    uint256 key = 123;

    function setUp() public {
        myContract = new MyContract();
        myContract.createMarket(123, 0x4567890000000000000000000000000000000000000000000000000000000000);

        market = myContract.getMarket(key);
    }

    function test_VmLoad() public {
        // slot 0
        console.log(ownerSlot);
        bytes32 ownerRaw = vm.load(address(myContract), bytes32(ownerSlot));
        address owner = address(uint160(uint256(ownerRaw)));
        console.logAddress(owner);
        assertEq(myContract.owner(), owner);

        uint256 keySlot = uint256(keccak256(abi.encode(key, mappingSlot)));

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207028
        uint256 marketIdSlotOffset = 0;
        bytes32 marketIdSlot = bytes32(keySlot + marketIdSlotOffset);
        console.log(uint256(marketIdSlot));
        uint256 marketId = uint256(vm.load(address(myContract), marketIdSlot));
        console.log(marketId);
        assertEq(market.marketId, marketId);

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207029
        uint256 priceFeedIdSlotOffset = 1;
        bytes32 priceFeedIdSlot = bytes32(keySlot + priceFeedIdSlotOffset);
        console.log(uint256(priceFeedIdSlot));
        bytes32 priceFeedId = vm.load(address(myContract), priceFeedIdSlot);
        console.logBytes32(priceFeedId);
        assertEq(market.priceFeedId, priceFeedId);
    }

    function test_StdStore() public {
        // slot 0
        console.log(ownerSlot);
        address owner = stdstore.target(address(myContract)).sig("owner()").read_address();
        console.logAddress(owner);
        assertEq(myContract.owner(), owner);

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207028
        console.log(stdstore.target(address(myContract)).sig("marketMap(uint256)").with_key(key).depth(0).find());
        uint256 marketId = stdstore
            .target(address(myContract))
            .sig("marketMap(uint256)")
            .with_key(key)
            .depth(0)
            .read_uint();
        console.log(marketId);
        assertEq(market.marketId, marketId);

        // slot 88533158270886526242754899650855253077067544535846224911147251622586185207029
        console.log(stdstore.target(address(myContract)).sig("marketMap(uint256)").with_key(key).depth(1).find());
        bytes32 priceFeedId = stdstore
            .target(address(myContract))
            .sig("marketMap(uint256)")
            .with_key(key)
            .depth(1)
            .read_bytes32();
        console.logBytes32(priceFeedId);
        assertEq(market.priceFeedId, priceFeedId);
    }
}

// run: forge test --mc MyTest -vv

ref:
https://docs.soliditylang.org/en/v0.8.19/internals/layout_in_storage.html
https://book.getfoundry.sh/cheatcodes/load
https://book.getfoundry.sh/reference/forge-std/std-storage

You could also find the slot numbers of each top-level state variables in OpenZepplin Upgrades Plugin's manifest file (the {network}.json) if your contracts are upgradeable.

In addition to that, Foundry's cast as well as provides a command to read storage at a certain slot:

// read the slot 2 (decimals) of WETH on Ethereum
// WETH slot 0: name
// WETH slot 1: symbol
// WETH slot 2: decimals
cast storage 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 2 --rpc-url https://rpc.flashbots.net/

ref:
https://book.getfoundry.sh/reference/cast/cast-storage
https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code

Surviving the Digital Dark Forest: Tips for Staying Safe Online

Surviving the Digital Dark Forest: Tips for Staying Safe Online

How I learned to "start worrying" and to embrace the illusion of safety.

Digital security and privacy have become more important than ever. With cyber threats constantly evolving, it is crucial to stay up-to-date with best practices and take proactive measures to protect your online presence. This comprehensive guide covers a wide range of security and privacy recommendations for various platforms and scenarios, aiming to help you fortify your digital life and assets.

General

  • Use a password manager: 1Password or Bitwarden.
    • Never reuse passwords.
    • Use strong passwords.
  • Always enable TOTP-based 2FA (Time-based One-Time Password).
    • Avoid SMS-based 2FA which is vulnerable to SIM swap attack.
    • Even you don't use SMS-based 2FA, your phone number might be used as a "Recovery Method".
  • Avoid using your password manager to generate one-time passwords for critical accounts.
  • Use Passkey.
  • Use security keys: YubiKey.
  • Use different email addresses when registering services if possible.
  • Always use HTTPS.
  • Don't blindly click links you see in your emails or search results; they could be scams!
    • Instead, add your frequently visited websites to your browser bookmarks.
  • Carefully review requested permissions when you connect third-party apps to your critical accounts.
  • Regularly review authenticated devices or sessions for your critical accounts.
    • Revoked them if you're not sure what they are.
    • Explicitly logout after finishing your operations or use Incognito mode.
  • Be skeptical of urgent requests, even from "known" contacts.
  • Have an incident response plan ready BEFORE you need it.
    • If your computer is compromised:
      • Immediately disconnect from network (disable Wi-Fi/unplug ethernet cable) to prevent further data exfiltration.
      • DO NOT reboot or shutdown: this preserves evidence in memory and may prevent malware from establishing persistence.
      • Document everything: take photos of screen, note the time, and any suspicious behavior observed
      • Use a different device to change critical passwords and revoke sessions.
  • Do things that can calm your anxiety.
  • Read Personal Security Checklist.
  • Read An ultimate list of rules any on-chain survivor should follow to stay safe.

Privacy

Credit Card

  • Use different credit cards with different merchants.
    • Some for online shopping.
    • Some for physical payments or Apple Pay/Google Pay.
  • Prefer credit cards over debit cards.
    • Credit card fraud protection is superior because fraudulent charges don't immediately deplete your bank account balance.
  • Set spending limits.

Crypto

  • For large amounts of assets, always store them in hardware wallets or multisig wallets.
  • Use multiple hardware wallets from different vendors: Trezor or Ledger.
    • Some should only hold assets and never interact with DeFi apps.
    • Some are used for trading or farming.
  • Use hardware wallet's hidden wallet with custom passphrase.
  • Always verify transaction details on hardware wallet screens, not just computer screens.
    • Even the Safe UI was spoofed in the infamous Bybit hack.
  • Write your seed phrases on paper or metal, and store them in a physical safe.
    • Keep at least 2 copies in different locations.
    • Never store a hardware wallet's seed phrase digitally, NEVER.
  • Verify backups of your seed phrases every 3 months.
  • Use multisig wallets: Gnosis Safe.
  • Only store a small amount of assets in hot wallets.
    • If you follow this rule, it might be acceptable to store the seed phrase in a password manager.
    • Furthermore, encrypt the seed phrase before storing it.
  • When transferring tokens to a new address, always send a small amount first, and make sure you can transfer them out.
    • It may waste gas, but it's better than losing funds.
  • Add addresses to contacts or whitelists.
  • Always approve tokens with the exact amount, never use infinite (type(uint256).max) approval.
    • It may waste gas, but it's better than losing funds.
  • Always check the slippage setting before swapping.
  • Review your token approvals regularly: Revoke.cash.
    • Before revoking an approval, you should check the original approve() tx is initiated by you.
    • Attackers can create a fake ERC-20 token and set allowance for you.
  • Signing could be dangerous.
    • If it's a clear, human-readable message, it's probably safe to sign.
    • If it contains a large amount of data, read carefully before signing.
    • If the message starts with 0x, just don't sign.
    • Especially, there are "permit" signatures.
  • Use browser extensions or wallets that can simulate/preview transactions.
  • Learn how to decode a transaction.
  • Use Etherscan's Watch List to monitor your account activities.
    • Though the notification might be a bit delayed, it's not real-time.
  • Website (domain name or frontend code) can be hacked as well, even if smart contracts are secure.
  • Read Blockchain Dark Forest Selfguard Handbook.

macOS

  • Use an application firewall and network monitor: Little Snitch.
  • Use an antivirus software: Bitdefender Antivirus.
  • Turn on Firewall.
    • System Settings > Network > Firewall > Options > Block all incoming connections
  • Turn on FileVault which provides full disk encryption.
    • System Settings > Privacy & Security > FileVault
  • Power off your computer when not in use, in order for the disk to be encrypted.
  • Automatically lock your screen when idle.
    • System Settings > Lock Screen > Require password after screen saver begins or display is turned off
  • Set one of Hot Corners to "Lock Screen" and always trigger it when you're away from the keyboard.
    • System Settings > Desktop & Dock > Hot Corners
  • Disable AirDrop and Handoff.
    • System Settings > General > Airdrop & Handoff
  • Exclude sensitive folders from Spotlight.
    • System Settings > Siri & Spotlight > Spotlight Privacy
  • Don't use any apps that can read your clipboard or what you type.
  • Don't use third-party input tools if possible.
  • Create separate browser profiles for different use cases.
    • One for daily activities.
    • One for financial activities, don't install any extensions other than the password manager.
    • Use Incognito mode.
    • Even better: use an isolated computer.
  • The fewer browser extensions installed, the better.
    • Carefully review requested permissions when installing/upgrading browser extensions.
    • Be aware of developers might sell their extension to someone else.
  • Disable Chrome's Preload pages.
    • Chrome > Settings > Performance > Preload pages
  • Install OS security patches as soon as possible.
  • Use Dangerzone if you're working with PDFs.
  • Read macOS Security and Privacy Guide.

iOS

  • Enable Data Protection (Erase all data after 10 failed passcode attempts).
    • Settings > Touch ID & Passcode > Erase Data
  • Change the default PIN of your SIM card.
    • Settings > Cellular > SIM PIN > Change PIN
  • Disable Predictive Text.
    • Settings > General > Keyboards > Predictive
    • Settings > General > Transfer or Reset iPhone > Reset > Reset Keyboard Dictionary
  • Turn off AirDrop.
  • Don't use third-party keyboard apps.
    • These apps will be able to access everything you type: passwords, messages, search terms, etc.
  • Restart your device regularly, ex: once a week.
  • Rapidly press the side button 5 times to enter Emergency SOS mode when needed.
    • Under Emergency SOS mode, your passcode is required to re-enable Touch ID or Face ID.
    • Use it when your device is about to be taken away.
  • Read Telegram & Discord Security Best Practices.
  • Read Privacy Guides - iOS Overview.

Developer

  • Always create API keys with minimum permissions and set a short expiration time if possible.
  • Create distinct API keys for different purposes, services, or machines.
    • Deactivate the API key if you're not using it.
  • Avoid storing credentials in plain text on disk, such as in .env files or ~/.aws/credentials.
    • Instead, store them in 1Password Environments and source them with 1Password CLI.
  • If you're unsure, run the program inside a non-root Docker container.
  • The fewer IDE/editor plugins installed, the better.
  • Be aware of Supply Chain Attack.
    • Run tools like npm audit or pip-audit to check.
  • Enable security-related features on your GitHub repos.
  • Sign your Git commits.

Wi-Fi

  • Avoid using Wi-Fi routers and IoT devices made in China if possible.
    • Due to documented security vulnerabilities and potential mandatory backdoor requirements.
  • Must change the default username/password of your devices.
  • Create a dedicated Wi-Fi network (guest network) for IoT devices.
  • Keep your device firmware up-to-date.
  • Use WPA3-Personal if possible.
  • Disable remote access on your router.
    • If you really want to visit your router's management console through the Internet, set IP whitelist at least.
  • Disable WPS (Wi-Fi Protected Setup) which is vulnerable to brute-force attack.
  • Avoid using public Wi-Fi.

Physical

  • Be cautious when plugging USB devices into your computer.
    • Don't charge devices from your computer if possible.
  • Be vigilant for key loggers.
    • Bring your own keyboard and USB hub when necessary.
  • Cover your laptop's camera with a sticky note.
  • Use certified and well-protected extension cords.
  • Get fire and earthquake insurance for your house.
  • Shred or redact sensitive documents.
    • Instead of simply disposing of them in the trash.

Personal

  • Never share where you live or post photos that could potentially reveal your location.
    • Like a photo of the view from your window, such data can be exploited via OSINT (Open-Source INTelligence).
    • If you really want to share where you've been (such as during travel), only post them after you're back home.
  • Don't reveal information during "inbound" calls.
    • Only share sensitive data during communications that you initiate.
  • Be aware that even if you take all precautions, you may still be vulnerable to a $5 wrench attack.
    • So keep things low-key.

And don't forget: security is always a trade-off.

Solidity: CREATE vs CREATE2

Solidity: CREATE vs CREATE2

In Solidity, there are two opcodes for creating contracts: CREATE and CREATE2. Also, the deployed contract address can be precomputed via:

  • keccak256(deployerAddress, deployerNonce) if you're using CREATE opcode
  • keccak256(0xFF, deployerAddress, salt, bytecode) if you're using CREATE2 opcode

ref:
https://ethereum.stackexchange.com/questions/101336/what-is-the-benefit-of-using-create2-to-create-a-smart-contract

CREATE

Default opcode used when deploying smart contracts. If you're deploying a contract using new YourContract() without salt, then you're using CREATE.

The following code written in TypeScript and ethers.js, shows how to deploy a contract using CREATE under the hood:

import { ethers } from 'ethers';

const deployer = await ethers.getNamedSigner("deployer")
const nonce = await deployer.getTransactionCount()
const computedAddress = ethers.utils.getContractAddress({
    from: deployer.address,
    nonce: nonce,
})
console.log(`computed address: ${computedAddress}`)

const ktbArbitrageurFactory = await ethers.getContractFactory("KtbArbitrageur", deployer)
const ktbArbitrageur = await ktbArbitrageurFactory.deploy(oinchAggregationRouterV5)
console.log(`deployed address: ${ktbArbitrageur.address}`)

Though it's pretty inefficient, but you can specify the deployed address (to some extend) by keeping increasing nonce until it meets some conditions you set:

async increaseNonceToDeployUpgradeable(condition: string, targetAddr: string) {
    const { ethers } = this._hre
    const deployer = await ethers.getNamedSigner("deployer")

    // We use deployer's address and nonce to compute a contract's address which deployed with that nonce,
    // to find the nonce that matches the condition
    let nonce = await deployer.getTransactionCount()
    console.log(`Next nonce: ${nonce}`)

    let computedAddress = "0x0"
    let count = 0
    while (
        count < 2 ||
        (condition == "GREATER_THAN"
            ? computedAddress.toLowerCase() <= targetAddr.toLowerCase()
            : computedAddress.toLowerCase() >= targetAddr.toLowerCase())
    ) {
        // Increase the nonce until we find a contract address that matches the condition
        computedAddress = ethers.utils.getContractAddress({
            from: deployer.address,
            nonce: nonce,
        })
        console.log(`Computed address: ${nonce}, ${computedAddress}`)
        nonce += 1
        count += 1
    }

    // When deploying a upgradable contract,
    // it will deploy the implementation contract first, then deploy the proxy
    // so we need to increase the nonce to "the expected nonce - 1"
    let nextNonce = await deployer.getTransactionCount()
    for (let i = 0; i < count - 1 - 1; i++) {
        nextNonce += 1
        console.log(`Increasing nonce to ${nextNonce}`)
        const tx = await deployer.sendTransaction({
            to: deployer.address,
            value: ethers.utils.parseEther("0"),
        })
        await tx.wait()
    }

    console.log(`Finalized nonce`)
}

ref:
https://docs.ethers.org/v5/api/utils/address/#utils-getContractAddress

CREATE2

The Solidity code below demonstrates how to deploy a contract using CREATE2 which is introduced in EIP-1014 to provide more flexible and predictable address generation:

bytes32 salt = bytes32("perp");
address oinchAggregationRouterV5 = 0x1111111254EEB25477B68fb85Ed929f73A960582;

address computedAddress = address(
    uint256(
        keccak256(
            abi.encodePacked(
                bytes1(0xff), // avoid conflict with CREATE
                address(this), // deployer
                salt,
                keccak256(
                    abi.encodePacked(
                        type(KtbArbitrageur).creationCode, // bytecode
                        abi.encode(oinchAggregationRouterV5) // constructor parameter
                    )
                )
            )
        )
    )
);

KtbArbitrageur ktbArbitrageur = new KtbArbitrageur{ salt: salt }(oinchAggregationRouterV5);
console.logAddress(address(ktbArbitrageur));
console.logAddress(computedAddress);

You can change salt to an arbitrary value to produce different contract addresses.

ref:
https://docs.soliditylang.org/en/v0.7.6/control-structures.html#salted-contract-creations-create2