syskey

syskey

twitter
github
discord server

A brief analysis of $smart contracts, why the amount is zero?

$smart is a token deployed on the BASE network by Sushiswap's CTO joseph.eth using the EIP-7702 proposal.

EIP=7702#

The implementation principle of EIP-7702 is based on an innovative mechanism that allows externally owned accounts (EOA) to temporarily have the functionality of a smart contract account during a single transaction, without permanently modifying the EOA. The core idea is to add a contract_code field, which assigns this code to the EOA during the transaction process, granting it smart contract functionality. Once the transaction is completed, the smart contract code is automatically removed, and the EOA returns to its original state.

How to correctly claim $smart#

Simple analysis of the $smart contract#


function prepapreClaim() external {
    assembly {
        tstore(0, 1)
    }
}

function claim() external {
    assembly {
        if iszero(tload(0)) {
            mstore(0x00, 0x33adb0bc) // `RIPBozo()`.
            revert(0x1c, 0x04)
        }
    }
    if (hasClaimed[msg.sender]) {
        revert OnlyOneClaimPerWallet();
    }
    volatilityAccumulator = volatilityAccumulator + (block.timestamp - lastClaim) >> 1;
    uint256 amount = (DEFAULT_AMOUNT * volatilityAccumulator) / TARGET_INTERVAL;
    amount = (amount > DEFAULT_AMOUNT) ? DEFAULT_AMOUNT : amount;
    uint256 supply = totalSupply();

    // mintcap already hit
    if (supply == MINT_CAP) {
        revert MintCapExceeded();
    }
    // ensure we hit the mintcap
    else if (supply + amount > MINT_CAP) {
        amount = MINT_CAP - supply;
    }
    hasClaimed[msg.sender] = true;
    lastClaim = block.timestamp;
    _mint(msg.sender, amount);
}

The main parts of the $smart contract are the prepapreClaim and claim functions. The prepapreClaim function changes the value of storage slot 0 to 1, while the claim function reads the value of storage slot 0 using tload(0). If the value is 0, it exits the claim function. Combining the prepapreClaim function, it can be concluded that to successfully mint the token, one must first call the prepapreClaim function to change the value of storage slot 0 to 1 in the same transaction, and then call the claim function to receive the token. To call both functions in the same transaction, a contract wallet must be used, as EOA wallets cannot execute two functions in the same transaction.

Why does using a contract wallet result in an amount of 0?#

To answer this question, we need to analyze how the amount is calculated in the claim function. The code that determines the amount in the claim function is mainly composed of the following four lines.

// Set the release rate volatilityAccumulator global variable will dynamically change the value of volatilityAccumulator
volatilityAccumulator = volatilityAccumulator + (block.timestamp - lastClaim) >> 1;

// Release rate * default amount
uint256 amount = (DEFAULT_AMOUNT * volatilityAccumulator) / TARGET_INTERVAL;

// If amount is greater than DEFAULT_AMOUNT, then amount equals DEFAULT_AMOUNT, otherwise it equals amount
amount = (amount > DEFAULT_AMOUNT) ? DEFAULT_AMOUNT : amount;
lastClaim = block.timestamp;

The release rate is a key parameter for calculating the amount. According to the code, the parameters that mainly affect the release rate are volatilityAccumulator and block.timestamp. The default volatilityAccumulator when the contract is deployed is 600, so the only factor that truly affects the release rate is block.timestamp.

  • Normal case release rate

Assuming lastClaim and block.timestamp differ by 1 second, the calculation for volatilityAccumulator is:

volatilityAccumulator = 600 + 1 >> 1;
volatilityAccumulator = 601 / 2;
volatilityAccumulator = 300.5;
  • Extreme case release rate

If a user continuously calls the claim function (block.timestamp equals lastClaim), then the value of volatilityAccumulator will tend towards 0 after many calls, because volatilityAccumulator is a global variable, and each call to the claim function dynamically changes the value of volatilityAccumulator. In this case, the amount will always be 0, which explains why some people receive an amount of 0.

volatilityAccumulator = 0;
uint256 amount = 0;
amount = 0;

The $smart deployed on the BASE network actually conforms to the extreme case, as there are bots continuously calling the contract to claim smart, leading to the value of volatilityAccumulator being nearly 0. To obtain the token, it is necessary that block.timestamp - lastClaim >= 1, and since the block time on the BASE network is 2 seconds, this explains why some say that only one claim can be made per block.

Analyzing how many tokens can be obtained in this extreme case when block.timestamp - lastClaim >= 1:

volatilityAccumulator = infinitely close to 0, but not equal to 0;

// uint256 amount = (DEFAULT_AMOUNT * volatilityAccumulator) / TARGET_INTERVAL
// The calculation of amount can be approximated to the following code
// The default DEFAULT_AMOUNT equals 500000000000000000000000 wei, which is 500000 ether
// TARGET_INTERVAL equals 600
// amount = 500000 / 600
// amount = 833.33333333
uint256 amount = DEFAULT_AMOUNT / TARGET_INTERVAL;

How to increase the success rate of claiming?#

From the above analysis, we can clearly see that as long as we ensure that our transaction is prioritized in each block, we can 100% claim 833.33333333 tokens. So, how can we ensure that our transaction is always at the top? Some have mentioned increasing the GAS price, but I am not too sure about this. Due to the variety of blockchain protocols, I am not familiar with the block packaging rules of the Base network, so I cannot determine whether simply increasing the GAS price is sufficient to influence the order of transactions.

Conclusion#

Thank you very much to those who left replies in this tweet.

Finally, you can verify the following code in Remix#

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

contract ClaimHelper {
    uint256 lastClaim;
    uint256 volatilityAccumulator;

    uint256 public immutable DEFAULT_AMOUNT;
    uint256 public immutable TARGET_INTERVAL;
    uint256 public amount;

    constructor(
        uint256 _targetInterval,
        uint256 _defaultAmount
    ) {
        TARGET_INTERVAL = _targetInterval;
        DEFAULT_AMOUNT = _defaultAmount;
        lastClaim = block.timestamp;
        volatilityAccumulator = _targetInterval;
    }

    event amountUpdata(uint256 newValue);

    function claim() external {
        volatilityAccumulator = volatilityAccumulator + (block.timestamp - lastClaim) >> 1;
        amount = (DEFAULT_AMOUNT * volatilityAccumulator) / TARGET_INTERVAL;
        amount = (amount > DEFAULT_AMOUNT) ? DEFAULT_AMOUNT : amount;
        lastClaim = block.timestamp;
        emit amountUpdata(amount);
    }

    function getVolatilityAccumulator() external view returns (uint256) {
        return volatilityAccumulator;
    }
    function getAmount() external view returns (uint256) {
        return amount;
    }
}

Set _targetInterval and _defaultAmount to 600, run the claim function multiple times in a row, and then check the amount.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.