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

Solidity: abi.encode() vs abi.encodePacked() vs abi.encodeWithSignature() vs abi.encodeCall()

Solidity: abi.encode() vs abi.encodePacked() vs abi.encodeWithSignature() vs abi.encodeCall()

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

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

How to Stay Safe Online: Tips for Personal Security

How to Stay Safe Online: Tips for Personal Security

How I learned to "start worrying" and love the illusion of feeling safe.

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.

General

  • Use a password manager: 1Password or Bitwarden.
    • Never reuse passwords.
    • Use strong passwords, at least 18 characters long.
  • Don't store real passwords of critical accounts in password managers.
    • Instead, store the password altered by a secret rule only you know.
    • Apply to accounts related to your real-world identity or money.
    • Cloud-based password manager could be hacked too, e.g. LastPass.
  • Use different emails and different passwords when registering services.
  • Avoid using your password manager to generate one-time password.
    • Instead, use a separate authenticator app: Yubico Authenticator.
    • Otherwise, the password manager would become a single point of failure if compromised.
  • Always enable 2FA (or MFA), but avoid SMS-based 2FA if possible.
    • Be aware of SIM swap attack.
    • Even you don't use SMS-based 2FA, your phone number might be used as a "Recovery Method".
    • Turn off "Cloud Syncing" feature if you're using Google Authenticator.
    • Write down backup codes on paper and store them in a safe.
  • Always use HTTPS.
  • Use Passkey or security keys.
    • You could use your Mac/iOS/Android devices or YubiKey.
  • Don't provide real personal information to any cloud service if possible.
  • Install any security updates as soon as possible.
  • Don't blindly click any link 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 Google, Twitter, Discord, or other critical accounts.
  • Regularly review authenticated devices or sessions of your critical accounts.
    • Revoked them if you're not sure what they are.
    • Explicitly logout after finishing your operations.
  • 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

macOS

  • Use an application firewall and network monitor: Little Snitch.
  • 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 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 usages.
    • One for daily activities.
    • One for financial activities, don't install any extensions other than the password manager for this profile.
    • Use Incognito mode.
    • Even better: use separate computers.
  • 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.
  • Use Dangerzone┬áif you are working with PDFs.
  • Use an ad blocker: uBlock Origin.
  • 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.

Crypto

  • For large amounts of assets, always store them in hardware wallets or multisig wallets.
  • Use multiple hardware wallets from different vendors; don't put all your eggs in one basket.
    • Some should only hold assets and never interact with DeFi apps.
    • Some are used for trading or DeFi stuff.
    • Or use an old phone to create wallets, and NEVER connect it to Internet.
  • Use hardware wallet's hidden wallet with passphrase.
  • Write your seed phrases on paper or metal, and store them in a physical safe.
    • Modify the seed phrase with a secret rule only you know.
    • 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.
  • Rotate your hot wallets regularly.
  • When transferring tokens to a new address, always send a small amount first, and make sure you can transfer 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 or frontend code) can be hacked, even if smart contracts are secure.
  • Read Blockchain Dark Forest Selfguard Handbook.
  • Read officercia.eth's articles.

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.
  • If you're unsure, run the program inside a non-root Docker container.
  • The fewer IDE/editor plugins installed, the better.
  • Enable GitHub Copilot only for specific languages or files.
    • Especially, disable it for .env files or any files that may contain sensitive data.
  • Sign your Git commits.

Wi-Fi

  • Always change the default username/password of your router or IoT devices.
  • Keep your router firmware up-to-date.
  • Only use WPA3-Personal or higher.
  • Disable remote access on your router.
    • If you really want to visit your router's management console through 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

  • Cover your laptop's camera with a sticky note.
  • 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.
  • Shred or redact sensitive documents.
    • Instead of simply disposing of them in the trash.
  • Don't reveal information during "inbound" calls.
    • Only share sensitive data during outbound calls or communications that you initiate.
  • Use a certified and well-protected extension cord.
  • Get fire and earthquake insurance for your house.
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