if __name__ == '__main__': gitbook_url = 'https://docs.zerolend.xyz'# Replace with the GitBook URL output_file = 'documentation.md'# Desired output markdown file
main(gitbook_url, output_file)
Replace above official document url to whatever you need.After modify it to your own url, run the following python script.
1
python3 gitbook_scraper.py
After successlly execute the python script you’ll see the documentation.md on your root directory.
Feed the markdown file to AI
Let’s take claude.ai as example.
Copy the generated flowchart to online mermaid website.
view the flowchart online
Jump to website https://mermaid.live/ , paste the copyed flowchart code to it.
Why we need this ?
As we know, Web3 security competitions are time-limited. Participants who grasp the entire system quickly gain an advantage in identifying more vulnerabilities. With the help of AI we can generate flowchart in minitues. Furthermore, we have a summary of the entire project. I’m not entirely convinced that AI will replace auditors in the near future, but I am certain that those who don’t utilize AI will be left behind.
The LayerZero Endpoint is an immutable smart contract that implements a standardized interface for Omnichain Applications (OApps) to manage security configurations and seamlessly send and receive messages.
Message Library
Each MessageLib is an immutable verification library that OApp owners can configure their application to use. The protocol enforces the contract owner’s unique OApp Configuration before sending and receiving messages.
Message Packet
The Message Packet standardizes the format and size of messages that are sent between different blockchains:
1 2 3 4 5 6 7 8 9
struct Packet { uint64 nonce; // the nonce of the message in the pathway uint32 srcEid; // the source endpoint ID address sender; // the sender address uint32 dstEid; // the destination endpoint ID bytes32 receiver; // the receiving address bytes32 guid; // a global unique identifier bytes message; // the message payload }
Security Stack (DVNs)
Security Stack (DVNs) is used to check the payloadHash emitted for message integrity. There are two number here:
required Decentralized Verifier Networks(DVNs)
optional Decentralized Verifier Networks(DVNs)
Executors
Executors is used to invoke target chain lzReceive function in the Endpoint contract once the message payload is verified.
Methods
_lzSend
the function your application must implement to send an omnichain message.
_lzReceive
the function to receive an omnichain message
_lzCompose
Since each composable call is created as a separate message packet via lzCompose, this pattern can be extended for as many steps as your application needs (B1 -> B2 -> B3, etc).
Message Execution Options
LayerZero provides robust Message Execution Options, which allow you to specify arbitrary logic as part of the message transaction, such as the gas amount and msg.value the Executor pays for message delivery, the order of message execution, or dropping an amount of gas to a destination address.
quote
estimate of how much gas a message will cost to be sent and received
price data pushlisher submit data to pyth’s oracel program
pyth’s oracel program combines all price data and compute the final price based on it’s own algorithm
pyth transfer price data from pythnet to mutiple target chains eg,:evm,bnb,arbiturm
hermes(web service) listens to price update.
price consumer get the latest price from hermes(web service),verify the lastest price data on chain via oracel program on each chain,the program then return the price once price data is valid
Pyth Methods On EVM Table
Method
Need check publishTime
Most recent price
getPrice
✗
✓
getPriceUnsafe
✓
✗
getPriceNoOlderThan
✗
✗
getEmaPrice
✗
✗
getEmaPriceUnsafe
✓
✗
getEmaPriceNoOlderThan
✗
✗
getUpdateFee
✗
✗
getValidTimePeriod
✗
✗
parsePriceFeedUpdate
✗
✗
parsePriceFeeUpdateUnique
✗
✗
updatePriceFeeds
✗
✓
updatePriceFeedsIfNecessary
✗
✗
Pyth Methods On EVM
getPrice
arguments:
price feed id
getPrice() revert due to the price has not been update within getValidTimePeriod() seconds which is typically 1 minute.
1 2 3
EVM call reverted with exception:
StalePrice()
let’s take eth/usd as example, the return price is 291769250000, and the price is 2917.69250000 usd. The only argument is price feed id which is a 32-byte id written as a hexadecimal string.For eth/usd it’s 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace. You can get the full list from https://pyth.network/developers/price-feed-ids page.
The input arguments and return data format for getPriceUnsafe() are identical to those of the getPrice() function. However, the key distinction lies in their behavior: getPriceUnsafe() may return a price from an arbitrary point in the past, potentially outdated. Consequently, the responsibility falls on the caller or price consumer to ensure the returned data meets their recency requirements. This involves comparing the current block.timestamp with the returned publishTime to verify that the price update is sufficiently recent for their specific use case.
getPriceNoOlderThan
arguments:
price feed id
age
This function reverts with a PriceFeedNotFound error if the requested feed id has never received a price update. Calling updatePriceFeed to fix this issue.
getEmaPrice
arguments:
price feed id
Get the latest exponentially-weighted moving average (EMA) price and confidence interval for the requested price feed id.
price feed id This function may return a price from arbitrarily far in the past Get the latest exponentially-weighted moving average (EMA) price and confidence interval for the requested price feed id.
getEmaPriceNoOlderTham
arguments:
price feed id
age Get the latest exponentially-weighted moving average (EMA) price and confidence interval for the requested price feed id The caller provides an age argument that specifies how old the price can be.The call can be revert due to StalePriceError.
getUpdateFee
arguments:
updateData
Note that updataData is retrieved from Hermes REST API.
1 2 3 4 5
EVM call succeeded with result:
{ feeAmount: 1, }
getValidTimePeriod
Get the default valid time period in seconds. Above some getting function revert if current on-chain price is older than this period.
1 2 3 4 5
EVM call succeeded with result:
{ validTimePeriod: 60, }
parsePriceFeedUpdates
arguments:
updateData* hex[]
priceIds* bytes32[]
minPublishTime* uint64
maxPublishTime* uint64
fee* wei
Parse updateData and return the price feeds for the given priceIds within, if they are all published between minPublishTime and maxPublishTime (minPublishTime <= publishTime <= maxPublishTime). Please note that this function not returned the most recent price,instead it returned a price for fixed time(minPublishTime <= publishTime <= maxPublishTime). If caller need a most recent price they should call updatePriceFeeds followed by getPrice or one of its variants.
Unlike updatePriceFeeds, calling this function will not update the on-chain price
parsePriceFeedUpdatesUnique.
arguments:
updateData* hex[]
priceIds* bytes32[]
minPublishTime* uint64
maxPublishTime* uint64
fee* wei
the price update is the earliest update after the minPublishTime.
Update the on-chain price feeds using the provided updateData Caller first retrieve a latest price from Hermes REST API,and then invoke this function to update the on-chain price if this price is more recent than on-chain price,otherwise the provided update will be ignored.
updatePriceFeedsIfNecessary
arguments:
updateData* hex[]
priceIds* bytes32[]
publishTimes* uint64[]
fee* wei
Update the on-chain price feeds using the provided updateData if the on-chain data is not sufficiently fresh. The caller provides two matched arrays, priceIds and publishTimes. This function applies the update if there exists an index i such that priceIds[i]’s last publishTime is before than publishTimes[i]. Callers should typically pass publishTimes[i] to be equal to the publishTime of the corresponding price id in updateData. If this condition is not satisfied, the call will revert with a NoFreshUpdate error.
1.SettleLongPremium is incorrectly implemented: premium should be deducted instead of added
summary
1 2 3 4 5
// current available assets belonging to PLPs (updated after settlement) excluding any premium paid int256 updatedAssets = int256(uint256(s_poolAssets)) - swappedAmount;
// add premium to be paid/collected on position close > int256 tokenToPay = -realizedPremium;
In the code, the int type is used for numbers, and the sign of the subtrahend is used to control whether the final result is an increase or a decrease.
root cause
As a result, an error in the sign of the input parameters leads to the final operation being contrary to the expectation.
learned
When we use the int type, we need to pay special attention to the sign of the values
When deployNewPool is called it uses the spot price of the pool, which can be manipulated through a flashloan and thus could return a highly inaccurate result.
root cause
uses spot price
learned
spot price can be manipulated through a flashloan, try to use TWAP price instead.
2._validatePositionList() does not check for duplicate tokenIds, allowing attackers to bypass solvency checks
summary
The user can pass in an array of tokenIds, but there is no check for duplicates.
root cause
no check for duplicates
learned
Think about if pass in array need to be checked for duplicates.
3.Removed liquidity can overflow when calling SemiFungiblePositionManager.mintTokenizedPosition function
summary
1 2 3 4 5 6
if (!isBurn) { // we can't remove more liquidity than we add in the first place, so this can't overflow unchecked { removedLiquidity += chunkLiquidity; } }
options can be repeatly which can lead to removedLiquidity sum up mutiple times.
root cause
unchecked block overflow
learned
need to double check the unchecked block make sure it can not be overflow
4.Wrong leg chunkKey calculation in haircutPremia function
summary
when loop tokenId , uses always the index 0 when calculating the leg chunkKey instead of using the actual leg index
root cause
logic error
learned
pay more attention to the logic.
5.Panoptic pool can be non-profitable by specific Uniswap governance
summary
The developers only considered the current range of Uniswap fees, but these fees may change through governance, affecting the existing logic.
root cause
fee may changes in the future.
learned
It is necessary to consider the impact of governance on the current fees.
1.DAO unable to withdraw their funds due to Convex admin action
summary
The administrator can cause a DoS (Denial of Service) in the protocol by passing malicious parameters
root cause
according to docs : admin’s action is RESTRICTED
learned
When a role’s behavior is RESTRICTED, it is necessary to examine the consequences of any suspicious actions
2.Inadequate Allowance Handling in convertAndForward Function of OCT_DAO & OCT_YDL
summary
stricted allowance assertion check lead to transaction failed
root cause
protocol suffers from inadequate handling of token allowances for the 1inch router,however they are not reset afterward.
learned
take care of the allowance assertion check
3.cannot forward extra rewards from both OCY_Convex to OCT_YDL
summary
1 2
- if (rewardAmount > 0) { IERC20(rewardContract).safeTransfer(OCT_YDL, rewardAmount); } + if (rewardAmount > 0) { IBaseRewardPool_OCY_Convex_C(rewardContract).rewardToken().safeTransfer(OCT_YDL, rewardAmount); }
root cause
use safeTransfer in a none erc20 contract
learned
4.ZivoeYDL::earningsTrancheuse() always assumes that daysBetweenDistributions have passed, which might not be the case
summary
The protocol relies on keepers to call distributeYield. However, there is no guarantee that the keeper will make the call immediately.
root cause
The calculation of the APY depends on block.timestamp.
learned
When the calculation of APY depends on the timestamp, ensure it is called immediately
5.ZivoeYDL::distributeYield yield distribution is flash-loan manipulatable
summary
distributeYield is depends on totalSupply, however totalSupply can be manipulable through a flashloan.A 1-transaction inflated staked amount allows to inflate stakers distribution at the loss of vesters distribution
root cause
distributeYield amount is calculated with totalSupply
6.distributeYield() calls earningsTrancheuse() with outdated emaSTT & emaJTT while calculating senior & junior tranche yield distributions
summary
The earningsTrancheuse function uses emaSTT and emaJTT to calculate earnings, and then updates the latest emaSTT and emaJTT. It is recommended to update the latest values first and then use them to calculate earnings.
root cause
the value is not lastest need to be updated.
7.User cannot withdraw stakingToken due to incorrect calculation of _totalSupply
summary
When the user revokes a stake, the total amount is subtracted instead of the currently withdrawable amount, leading to an overflow DoS.
One of the key differences between Blast L2 and other Layer 2 solutions is that in Blast L2, ETH functions as a native rebasing token.
1.1 what is rebasing token?
A rebasing token in the Ethereum Virtual Machine (EVM) context is a type of cryptocurrency whose supply automatically adjusts, or rebases periodically to maintain a target price or peg. This mechanism is designed to achieve certain economic goals, such as price stability or controlled inflation/deflation.
1.2 why blast l2 ?
Blast users and smart contracts can earn ~4% from beacon chain staking yield
Blast redirects sequencer fees to the dapps that induced them
2.three options for yield modes
Smart contract must interact with 0x4300000000000000000000000000000000000002 to change their gas mode.
2.1 Void (DEFAULT)
Void is default yield modes for smart contract which deploy on blast l2. The balance of smart contract never changes, no yield is earned . Any normal dapp can be deploy on blast without any changes. Thus if you already have smart contract you can just move it to blast.
2.2 Automatic
Since eth is native rebasing on blast l2 , if deposit 1 eth into smart contract, the contract’s balance will grow to 1.04 ETH in a year.
2.3 Claimable
For claimable mode, if deposit 1 eth into smart contract , the contract’s balance will emain at 1 eth even after a year. We can implement two functions to claim yield:
claimYield
claimAllYield
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
interface IBlast{ // See IBlast interface source code below }
function claimYield(address recipient, uint256 amount) external { //This function is public meaning anyone can claim the yield IBlast(0x43...02).claimYield(address(this), recipient, amount); }
function claimAllYield(address recipient) external { //This function is public meaning anyone can claim the yield IBlast(0x43...02).claimAllYield(address(this), recipient); } }
1.getBunniTokenPrice wrongly returns the total price of all tokens
The function getBunniTokenPrice() is supposed to return the price of 1 Bunni token (1 share) like all other feeds, but it doesn't. It returns the total price of all minted tokens/shares for a specific pool (total value of position's reserves) which is wrong.
2.Possible incorrect price for tokens in Balancer stable pool due to amplification parameter update
3.Incorrect StablePool BPT price calculation
`getProtocolOwnedLiquidityOhm` is in scope , but any user can invoke deposit to add assets, deposit is not in scope.need to check those out of scope functions.
4.getReservesByCategory() when useSubmodules =true and submoduleReservesSelector=bytes4(0) will revert
when add not check parameters lead to some category can't get Reserves
5.Price calculation can be manipulated by intentionally reverting some of price feeds
when use uniswap and balancer to calculate price, both price feed can revert due to re-enter lead to user can manipulate price.
use selfdestruct can send eth to any contract,even contract not implement receive{}
1.Bidder can use donations to get VerbsToken from auction that already ended.
protocol use address(this).balance to check if bid amount is bigger than current eth value in contract , attacker can send eth to protocol to bypass above check. need to take care of address(this).balance check.
2.Incorrect amounts of ETH are transferred to the DAO treasury in ERC20TokenEmitter::buyToken(), causing a value leak in every transaction
need to track eth
3.VerbsToken.tokenURI() is vulnerable to JSON injection attacks
4.encodedData argument of hashStruct is not calculated perfectly for EIP712 singed messages in CultureIndex.sol
1.Attacker can reenter to mint all the collection supply
when mint ERC721 token,protocol invoke minter’s onERC721Received function can lead to re-entrancy issue.
2.Attacker can drain all ETH from AuctionDemo when block.timestamp == auctionEndTime
3.Adversary can block claimAuction() due to push-strategy to transfer assets to multiple bidders
claimAuction() implements a push-strategy instead of a pull-strategy for returning the bidders funds. protocol invoke user’s address to send eth,user can implement revert on receive{} function to brick the process.
4.Multiple mints can brick any form of salesOption 3 mintings
1.Weighted pool spot price calculation is incorrect
Notional calculate the spot price of Weighted pool using the balances of token. Which need to be upscaled according to the balancer but Notional doesn’t.
Not farmilar with balancer wighted pool.
2.Single-sided instead of proportional exit is performed during emergency exit
Single-sided instead of proportional exit is performed during emergency exit, which could lead to a loss of assets during emergency exit and vault restoration.
Comment should use proportionally withdraw but used single token out , pay more attention on comments.
3.Native ETH not received when removing liquidity from Curve V2 pools
Not farmilar with Curve V2 pool removing LP.
4.Different spot prices used during the comparison
5.Incorrect invariant used for Balancer’s composable pools
Not farmilar with balancer need to check balancer contract next time.
6.Fewer than expected LP tokens if the pool is imbalanced during vault restoration
The vault restoration function intends to perform a proportional deposit
1.FULL_RESTRICTED Stakers can bypass restriction through approvals
The openzeppelin ERC4626 contract allows approved address to withdraw and redeem on behalf of another address so far there is an approval。
Encountering an unfamiliar EIP requires a thorough understanding of its code before proceeding with the subsequent audit.
2.Soft Restricted Staker Role can withdraw stUSDe for USDe
The code does not satisfy that condition, when a holder has the SOFT_RESTRICTED_STAKER_ROLE, they can exchange their stUSDe for USDe using StakedUSDeV2。
When faced with strictly limiting conditions, it is essential to consider whether there are any possible ways to bypass them through various means.
3.users still forced to follow previously set cooldownDuration even when cooldown is off (set to zero) before unstaking
The main issue with this question is that when setting global parameters, the impact on previous users was not taken into consideration
4.Malicious users can front-run to cause a denial of service (DoS) for StakedUSDe due to MinShares checks
This issue arises from the fact that the first user of the vault can have a certain impact on subsequent users. Therefore, it is important to be mindful of the effects of the initial configuration when handling shares.
<address>.codehash != bytes32(0) check is insufficient to determine if an address has existing code
An account is considered empty when it has no code and zero nonce and zero balance.
if anyone transfers 1 wei to an address, .codehash will return keccak256("") instead of bytes32(0)
Fix for this is easier than suggested - just change from x.codehash != bytes32(0) to x.code.length != 0
2.Borrower has no way to update maxTotalSupply of market or close market
Developer define a function but no way to interact with it
3.When withdrawalBatchDuration is set to zero lenders can withdraw more then allocated to a batch
wirte a full test
4.Any address can withdraw from markets contrary
function access control
5.Borrower can drain all funds of a sanctioned lender
1.protocol use CREATE3 that’s not avaliable in the Zksync Era.
2.two modifer not cover block.timestamp == allocationEndTime.
1 2 3 4 5 6 7 8 9 10 11 12 13
function _checkOnlyActiveAllocation() internal view { if (allocationStartTime > block.timestamp || block.timestamp > allocationEndTime) { revert ALLOCATION_NOT_ACTIVE(); } }
/// @notice Checks if the allocation has ended and reverts if not. /// @dev This will revert if the allocation has not ended. function _checkOnlyAfterAllocation() internal view { if (block.timestamp < allocationEndTime) { revert ALLOCATION_NOT_ENDED(); } }
When block.timestamp == allocationEndTime none of above modifers revert.
3.Manager vote every recipient to update his state once reacted the threshold
Since we have serval states protocol not check if user have already reacted the threshold.
4._distribute amount not correctly calculated.
5.missing access modifer
6.Can not create a pool by cloning strategies on zkSync network
Can not create pool by cloning strategies on zkSync network because of different behaviors from EVM instructions between zkSync and Ethereum
7.RFPSimpleStrategy milestones can be set multiple times
1
if (upcomingMilestone != 0) revert MILESTONES_ALREADY_SET();
the value of upcomingMilestone not be updated after set milestones lead to manager can set milestones mutiple times.
8.fundPool does not work with fee-on-transfer token
1.Chain support chain cannot be removed or cleared in bridge contracts.
Contracts provide a way to add support for a chain; however, they do not provide a way to delete it. When a contract offers an addition method, it’s important to consider whether it should also provide a deletion method
2.Contract use msg.sender as remote chain receiver when bridge token.
AA wallet like safe has different wallet address on different chains. This vulnerability requires us to be familiar with some commonly used third-party tools in order to discover it
3.Two different transactions can result in the same txnHash value, thus breaking the approval process of transaction minting
Consider whether the keys in similar mappings are always unique?
4.Admin can’t burn tokens from blocklisted addresses because of a check in _beforeTokenTransfer
The burn operation prematurely calls the beforeTransfer function, but beforeTransfer needs to check if the address is blocked, which hinders the successful execution of the burn
Percentage of xETH in the Curve pool is between REBALANCE_UP_THRESHOLD and REBALANCE_DOWN_THRESHOLD. Rebalance operations are allowed When percentage of xETH is out of range.But the amount of xETH not checked which could lead to Percentage out of range after rebalance.
2.Invalid Validation
The value of the check does not match the expectation
3.ERC20
zero amount token transfer can cause DOS.
4.ERC20
check allowance == 0 should only use in initial or resetting to zero.
5.ERC20
when totalSupply() == 0
6.ERC20
bc of xETH.balanceOf(address(this)),staker can inflate the exchange rate by transferring tokens directly to the contract
7.MEV bot
uncorrect calculate slippage value.
8.Context
an array has add but no remove function.
9.ERC20
transfer token to target contract but can’t withdraw
The recipient of the flash loan will burn a certain amount of tokens as a fee. The recipient can be set to an arbitrary user who impelement receiver.onFlashLoan function.
In certain token-consuming operations, it’s necessary to consider whether only oneself is able to perform this action
2.Context
3.Common
Incorrectly implemented modifiers.
Always check modifiers revert?
4.Upgradable
Logic contact constructor should not update storage variable,cuz it will not be reflected in the proxy’s state.Instead we need use initializer function.
5.Precision Lost
user share can be burned cuz _EUSDAmount.mul(_totalShares).div(totalMintedEUSD) to zero.
6.Context
When using conditional statements, verify whether the values on the left and right sides of the equation meet the expected criteria.
7.Context
threshold value if different from the explanation in the document.
8.Dos
use IVault(pool).vaultType() vaultType can’t be a internel variable.
9.Math(M4)
when calculate token per second use wrong total amount.
As we know , blockchains are self-contained worlds.Smart contract can’t get information by invoking api like we do in web2.At the same time,centralized data providers are also not very reliable.For example,we need to know the price ETH/USDC when we swaping on defi like uniswap.Can we trust one single centralized data provider?The answer is obvious.So we need some decentralized data providers.Oracel act as an message bridge between blockchain and off-chain data provider.
why do SMART CONTRACTS need oracle?
To achieve deterministic execution, blockchains limit nodes to reaching consensus on simple binary (true/false) questions using only data stored on the blockchain itself.If blockchain get information from external data provider it would be impossible to achieve determinism.Different blockchain node maybe return different executes result.To solve those problems we create oracles.Oracle smart contract taking information from off-chain sources and then store the information on the blockchain.Since information stored on-chain is unalterable and publicly available, Ethereum nodes can safely use the oracle imported off-chain data to compute state changes without breaking consensus
how to use chainlink data feed oracle?
Let’s dive into an example,assume that we need to get the BTC/USD price on our smart contract. Chainlink provide data feed server on several networks.
Ethereum Mainnet
Sepolia Testnet
Gogerli Testnet
We gonna test the data feed server on goerli testnet by foundry tool.
First we need to import chainlink contract interface @chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol.
Secondly,let’s init the smart contract using a specify target smart contract address which we copy from chainlink official document website.
Lastly,we just need invoke latestRoundData function which return the current price about BTC/USD.
function getLatestData() public view returns (int) { // prettier-ignore ( /* uint80 roundID */, int answer, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = dataFeed.latestRoundData(); return answer; } }
we can use forge create command to deploy the contract.And then we invoke the getLatestData function via cast call command.Trust me you’ll get some return like this:
If you are using Chainlink Data Feeds on L2 networks like Arbitrum, Optimism, and Metis, you must also check the latest answer from the L2 Sequencer Uptime Feed to ensure that the data is accurate in the event of an L2 sequencer outage
Basiclly,access control means “who is allowed to do this thing”.It’s quite important for smart contract to specify a particular address who can totally control the whole system.Therefor it’s critical to fully understand how to use access control before using it in your project or just copy some example code from somewhere. In openzeppelin there are mainly two ways to implementing access control.
and the we use owner address to create the smart contract,it’s quite simple you don’t even need to pass any parameters.openzeppelin Ownable would set msg.sender as it’s owner as default.we use foundry for testing,if you’re not familiar with foundry,go and learn it first before proceeding further.
Firstly,we use owner address create our smart contract above in foundry test contract setUp function.
1 2 3 4 5
//let's set address owner to msg.sender. vm.prank(owner);
//create the contract. myContract = new MyContract();
And then we use the arbitrary address to invoke the specialThing function which is protected by onlyOwner modifier.As expect the invoke revert because of access control.Here goes the code:
1 2 3 4
//use arbitrary address invoke specialThing() function. vm.prank(arbitrary); vm.expectRevert("Ownable: caller is not the owner"); myContract.specialThing();
Finally,we use our owner who master the contract invoke the specialThing function.As you see the owne succeed.
1 2 3
//switch to owner. vm.prank(owner); myContract.specialThing();
contract MyToken2 is ERC20, AccessControl { // Create a new role identifier for the minter role bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor(address minter) ERC20("MyToken", "TKN") { _grantRole(DEFAULT_ADMIN_ROLE, minter); // Grant the minter role to a specified account _grantRole(MINTER_ROLE, minter); }
function mint(address to, uint256 amount) public { // Check that the calling account has the minter role require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter"); _mint(to, amount); } }
We can create whatever role we want in Role-Based Access Control (RBAC) contract.Let’s say we want create a minter role who can mint some ERC20 token.We can use a special constant bytes32 as the name of the role like this:
1 2
// Create a new role identifier for the minter role bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
Firstly,we use owner address as our minter to create our access control contract.
1
myToken = new MyToken2(owner);
And then we use the arbitrary user to mint some ERC20 token:
1 2 3 4
//use arbitrary address invoke mint() function. vm.prank(arbitrary); vm.expectRevert("Caller is not a minter"); myToken.mint(address(this), 1);
the above invoke would be revert because of arbitrary don’t hold the minter role.
1 2 3
//switch to owner. vm.prank(owner); myToken.mint(address(this), 1);
If we switch msg.sender to owner the call would successed.
What’s more? we can use owner address to grant a minter role whoever we want.Let’s say we grant the minter role to the arbitrary address:
There are 2 major ways to deploy smart contract CREATE and CREATE2. They look quite similar, but there are still some differences.CREATE2 and CREATE are one of solidity opcode which give us the ability to predict the address where a contract will be deployed before we deploy a smart contract.You might be curious about what opcode is.Opcodes are the fundamental building blocks of EVM bytecode, and each opcode represents a specific operation that the EVM can perform. For example, there are opcodes for arithmetic operations, logical operations, storage access, memory manipulation, conditional branching, and more.
What is CREATE
Smart contracts can be created both by other contracts and regular EOA.They both compute the new address the same way:
1
new_address = keccak256(sender, nonce)
So each created address is associated with a nonce value.Nonce increased on every transaction.It is related to the number of transactions we make and is unpredictable. That’s why we need CREATE2.
What is CREATE2 ?
The whole idea behind this opcode is to make the resulting address independent of future events. Regardless of what may happen on the blockchain, it will always be possible to deploy the contract at the precomputed address.There are four parameters in CREATE2 function:
Imagine a scenario where you need to deploy a contract to multiple networks, and precisely at that moment, you need to store the addresses of the yet-to-be-deployed contracts as storage parameters within the currently deployed contract. In such cases, you would require knowing the future addresses of the contracts to be deployed in advance.
function deploy( bytes memory bytecode, uint _salt ) public payable returns (address) { address addr;
assembly { addr := create2( callvalue(), // wei sent with current call // Actual code starts after skipping the first 32 bytes add(bytecode, 0x20), mload(bytecode), // Load the size of code contained in the first 32 bytes _salt // Salt from function arguments )
Merkle trees, named after Ralph Merkle, are a fundamental data structure used in cryptography and computer science. They are commonly employed in blockchain systems to ensure data integrity and enable efficient verification.
The principle behind Merkle trees is based on the concept of hash functions. A hash function is a mathematical algorithm that takes an input (data) and produces a fixed-size output, known as a hash value or digest. The key properties of a hash function are collision resistance and the avalanche effect.
how to generate merkle tree data via solidity
we can calculate hash value through solidity keccak256 function:
1 2 3 4 5 6 7 8
keccak256(abi.encodePacked(toHashValue)
e.g.: before hash 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
after hash 0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb
After performing the hash operation on the values of the leaf nodes, the adjacent nodes are then hashed together until only a single root node remains.
Suppose there are two adjacent nodes, A and B. The order of the hash operation, whether it is hash(A+B) or hash(B+A), is determined by the sizes of A and B. In the corresponding Merkle code in OpenZeppelin, we can find the following code snippet:
1 2 3
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { return a < b ? _efficientHash(a, b) : _efficientHash(b, a); }
In summary, the smaller value is typically placed in front to determine the order for computing the hash values. This may cause some confusion when performing the calculations manually.
In practical projects, it is common to store only the final result, the root hash value, in the contract. Storing a large number of addresses in the contract would consume a significant amount of gas fees. By using a Merkle tree calculation, the amount of data that needs to be stored is greatly reduced.
Let’s demonstrate how to calculate and store the root hash value using a setup example from Foundry:
bytes32 public root; bytes32[] public leafs; bytes32[] public l2;
function setUp() public { address[] memory addrss = new address[](4); addrss[0] = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2; addrss[1] = 0x2d886570A0dA04885bfD6eb48eD8b8ff01A0eb7e; addrss[2] = 0xed857ac80A9cc7ca07a1C213e79683A1883df07B; addrss[3] = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990;
//Calculating the hash values of leaf nodes from a list of addresses. leafs.push(keccak256(abi.encodePacked(addrss[0]))); leafs.push(keccak256(abi.encodePacked(addrss[1]))); leafs.push(keccak256(abi.encodePacked(addrss[2]))); leafs.push(keccak256(abi.encodePacked(addrss[3])));
//Calculating the hash values of the second level. l2.push(keccak256(abi.encodePacked(leafs[0], leafs[1]))); l2.push(keccak256(abi.encodePacked(leafs[2], leafs[3])));
For demonstration purposes, we have provided only four addresses. In actual projects, the number of addresses can be significantly larger.
how to proof merkle data
Once we have stored the root hash value in the contract, how do we verify if a client-provided address is a valid address or belongs to a whitelist?
Firstly, we need to hash the address as the third parameter. Then, we pass the hash value of the address, along with the adjacent hash values, as the proof to the verification function.
The proof list corresponds to the red-marked area in the image below.
test proof function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
function testVerify() public { address proofAddress = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
function parentHash(bytes32 a, bytes32 b) public pure returns (bytes32) { if (a < b) { return keccak256(abi.encode(a, b)); } else { return keccak256(abi.encode(b, a)); } }
The statement “abi.encode(address, uint)” will produce 64 bytes. Since “abi.encode(bytes32, bytes32)” also yields 64 bytes, hash collisions may potentially occur between leaf nodes and parent nodes.