diff --git a/.gitignore b/.gitignore index 9e7abee..edb9b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ docs/ # Dotenv file .env -.DS_Store \ No newline at end of file +.DS_Store + +# Create2 build files +.tmp +create2 diff --git a/.gitmodules b/.gitmodules index a4badc1..22e283c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/solady"] - path = lib/solady - url = https://github.com/Vectorized/solady [submodule "lib/LayerZero-v2"] path = lib/LayerZero-v2 url = https://github.com/clustersxyz/LayerZero-v2 @@ -17,3 +11,15 @@ path = lib/devtools url = https://github.com/0xfoobar/devtools branch = patch-1 +[submodule "lib/soledge"] + path = lib/soledge + url = https://github.com/Vectorized/soledge +[submodule "lib/multicaller"] + path = lib/multicaller + url = https://github.com/vectorized/multicaller +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/vectorized/solady diff --git a/lib/devtools b/lib/devtools index f2d2684..cb9dcb9 160000 --- a/lib/devtools +++ b/lib/devtools @@ -1 +1 @@ -Subproject commit f2d2684f9b57ccfc10afcb9f4290c12bc12162f0 +Subproject commit cb9dcb9f5982f9f086f0ce9fc0b5fdf1f60f84b0 diff --git a/lib/forge-std b/lib/forge-std index 6e05729..bf66061 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 6e05729b76f1ae0d437e74951aef1ca987788ab3 +Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 diff --git a/lib/multicaller b/lib/multicaller new file mode 160000 index 0000000..356350c --- /dev/null +++ b/lib/multicaller @@ -0,0 +1 @@ +Subproject commit 356350c2954d9e117ec839be37bddcdc773e04ac diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index dc62599..72c152d 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit dc625992575ecb3089acc35f5475bedfcb7e6be3 +Subproject commit 72c152dc1c41f23d7c504e175f5b417fccc89426 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 1bf98f4..22489db 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 1bf98f4edcf9d9e757314a49354ea98a3a771db4 +Subproject commit 22489db15621b9a42ebddb1facade6962034e9b9 diff --git a/lib/solady b/lib/solady index 74769c2..d355d14 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 74769c21b5759e897e90fb25026f24d889fe4b26 +Subproject commit d355d147f150844ddf55ffbb63fcd0130ac73fb4 diff --git a/lib/soledge b/lib/soledge new file mode 160000 index 0000000..641f4fc --- /dev/null +++ b/lib/soledge @@ -0,0 +1 @@ +Subproject commit 641f4fc39f6ca9b387115985a099b4dfd26135fb diff --git a/remappings.txt b/remappings.txt index 0a67d8b..b74bc46 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,6 +5,8 @@ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ solmate/=lib/solmate/src/ solady/=lib/solady/src/ +multicaller/=lib/multicaller/src/ +soledge/=lib/soledge/src/ clusters/=src/ solidity-bytes-utils/=lib/LayerZero-v2/oapp/node_modules/solidity-bytes-utils/ @openzeppelin/contracts/=lib/LayerZero-v2/oapp/node_modules/@openzeppelin/contracts/ diff --git a/src/ClustersMarketV1.sol b/src/ClustersMarketV1.sol new file mode 100644 index 0000000..d6ba55d --- /dev/null +++ b/src/ClustersMarketV1.sol @@ -0,0 +1,608 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; +import {Initializable} from "solady/utils/Initializable.sol"; +import {FixedPointMathLib as F} from "solady/utils/FixedPointMathLib.sol"; +import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {SafeCastLib} from "solady/utils/SafeCastLib.sol"; +import {LibBit} from "solady/utils/LibBit.sol"; +import {LibString} from "solady/utils/LibString.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {MessageHubLibV1 as MessageHubLib} from "clusters/MessageHubLibV1.sol"; + +/// @title ClustersMarketV1 +/// @notice All prices are in Ether. +contract ClustersMarketV1 is UUPSUpgradeable, Initializable, Ownable, ReentrancyGuardTransient { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STRUCTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev A struct to return the information of a name. + struct NameInfo { + // The id of the name. + uint256 id; + // The current owner of the name. + address owner; + // The timestamp of the start of current ownership. + uint256 startTimestamp; + // Whether the name is registered. + bool isRegistered; + // Price integral last price. + uint256 lastPrice; + // Price integral last update timestamp. + uint256 lastUpdated; + // Bid amount. + uint256 bidAmount; + // Bid last update timestamp. + uint256 bidUpdated; + // Bidder on the name. + address bidder; + // Amount backing the name. + uint256 backing; + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The storage struct for a bid. + struct Bid { + // Price integral last price. + uint88 lastPrice; + // Price integral last update timestamp. + uint40 lastUpdated; + // Amount backing the name. + uint88 backing; + // Bid last update timestamp. + uint40 bidUpdated; + // Bidder on the name. + address bidder; + // Bid amount. + uint88 bidAmount; + } + + /// @dev The storage struct for the contract. + struct ClustersMarketStorage { + // The stateless pricing contract and NFT contract. Packed. + // They both have at least 4 leading zero bytes. Let's save a SLOAD. + // Bits Layout: + // - [0..127] `pricing`. + // - [128..255] `nft`. + uint256 contracts; + // Mapping of `clusterName` to `bid`. + mapping(bytes32 => Bid) bids; + // The total amount that is could be refunded to bidders. To prevent over withdrawing. + // `address(this).balance >= totalBidBacking + amountWithdrawableByProtocol`. + uint88 totalBidBacking; + // The minimum bid increment (in Ether wei). + uint88 minBidIncrement; + // The number of seconds that must pass since the last bid update for the bid to be reduced. + uint32 bidTimelock; + } + + /// @dev Returns the storage struct for the contract. + function _getClustersMarketStorage() internal pure returns (ClustersMarketStorage storage $) { + assembly ("memory-safe") { + // `uint72(bytes9(keccak256("Clusters.ClustersMarketStorage")))`. + $.slot := 0xda8b89020ecb842518 // Truncate to 9 bytes to reduce bytecode size. + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* EVENTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The pricing contract has been set. + event PricingContractSet(address newContract); + + /// @dev The NFT contract has been set. + event NFTContractSet(address newContract); + + /// @dev The name has been purchased. + event Bought(bytes32 indexed clusterName, address indexed by, uint256 price); + + /// @dev The backing for `clusterName` has been increased. + event Funded(bytes32 indexed clusterName, address indexed by, uint256 oldBacking, uint256 newBacking); + + /// @dev The `clusterName` has been poked. + event Poked(bytes32 indexed clusterName, address indexed by); + + /// @dev A new bid has been placed by `bidder`. + event BidPlaced(bytes32 indexed clusterName, address indexed bidder, uint256 bidAmount); + + /// @dev The bid has been refunded to the previous bidder, `to`. + event BidRefunded(bytes32 indexed clusterName, address indexed to, uint256 refundedAmount); + + /// @dev The bid amount has been increased. + event BidIncreased(bytes32 indexed clusterName, address indexed bidder, uint256 oldBidAmount, uint256 newBidAmount); + + /// @dev The bid amount has been reduced. + event BidReduced(bytes32 indexed clusterName, address indexed bidder, uint256 oldBidAmount, uint256 newBidAmount); + + /// @dev The bid has been revoked. + event BidRevoked(bytes32 indexed clusterName, address indexed bidder, uint256 bidAmount); + + /// @dev The bid has been accepted. + event BidAccepted( + bytes32 indexed clusterName, address indexed previousOwner, address indexed bidder, uint256 newBacking + ); + + /// @dev The bid timelock has been updated. + event BidTimelockSet(uint256 newBidTimelock); + + /// @dev The minimum bid increment has been set. + event MinBidIncrementSet(uint256 newMinBidIncrement); + + /// @dev `amount` has been withdrawn from. + event NativeWithdrawn(address to, uint256 amount); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERRORS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The contract address must have at least 4 leading zero bytes. + error ContractAddressOverflow(); + + /// @dev The name is has already been registered. + error NameAlreadyRegistered(); + + /// @dev The name is not registered. + error NameNotRegistered(); + + /// @dev The `clusterName` is invalid. + error InvalidName(); + + /// @dev The payment is insufficient. + error Insufficient(); + + /// @dev Cannot bid on a name owned by oneself. + error SelfBid(); + + /// @dev The name has no bid. + error NoBid(); + + /// @dev The bid timelock has not passed. + error BidTimelocked(); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INITIALIZER */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Initializes the contract, seting the initial owner. + function initialize(address initialOwner) public initializer onlyProxy { + _initializeOwner(initialOwner); + _getClustersMarketStorage().minBidIncrement = 0.0001 ether; + _getClustersMarketStorage().bidTimelock = 30 days; + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* MARKET */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Purchases the `clusterName`. + function buy(bytes32 clusterName) public payable nonReentrant { + uint256 contracts = _getClustersMarketStorage().contracts; + uint256 packedInfo = _packedInfo(contracts, clusterName); + if (_isRegistered(packedInfo)) revert NameAlreadyRegistered(); + + uint256 minAnnualPrice = _minAnnualPrice(contracts); + if (msg.value < minAnnualPrice) revert Insufficient(); + + Bid storage b = _getClustersMarketStorage().bids[clusterName]; + b.lastPrice = SafeCastLib.toUint88(minAnnualPrice); + b.lastUpdated = uint40(block.timestamp); + b.backing = SafeCastLib.toUint88(msg.value); + + address to = MessageHubLib.senderOrSigner(); + if (_id(packedInfo) == uint256(0)) { + _mintNext(contracts, clusterName, to); + } else { + _move(contracts, to, packedInfo); + } + emit Bought(clusterName, to, msg.value); + } + + /// @dev Increases the backing for `clusterName`. + function fund(bytes32 clusterName) public payable nonReentrant { + (,, Bid storage b, address sender) = _registeredCtx(clusterName); + uint256 oldBacking = b.backing; + uint256 newBacking = F.rawAdd(oldBacking, msg.value); + b.backing = SafeCastLib.toUint88(newBacking); + emit Funded(clusterName, sender, oldBacking, newBacking); + } + + /// @dev Flushes the ownership of `clusterName`. + /// If `clusterName` has expired, it will be moved to the highest bidder (if any), + /// or to a stash for reclaimmed names. + function poke(bytes32 clusterName) public nonReentrant { + (uint256 contracts, uint256 packedInfo, Bid storage b, address sender) = _registeredCtx(clusterName); + _poke(contracts, packedInfo, clusterName, b, sender); + } + + /// @dev Internal helper for poke. + function _poke(uint256 contracts, uint256 packedInfo, bytes32 clusterName, Bid storage b, address sender) + internal + returns (bool moved) + { + (uint256 spent, uint256 newPrice) = + _getIntegratedPrice(contracts, b.lastPrice, F.rawSub(block.timestamp, b.lastUpdated)); + uint256 backing = b.backing; + + // If out of backing (expired), transfer to highest sufficient bidder or delete registration. + if (spent >= backing) { + moved = true; + (address bidder, uint256 bidAmount) = (b.bidder, b.bidAmount); + bool hasBid = bidder != address(0); + b.lastPrice = SafeCastLib.toUint88(_minAnnualPrice(contracts)); + b.lastUpdated = uint40(block.timestamp); + b.backing = uint88(F.ternary(hasBid, bidAmount, 0)); + b.bidUpdated = 0; + b.bidder = address(0); + b.bidAmount = 0; + _decrementTotalBidBacking(bidAmount); + // Transfer the name to the bidder, if there's a bid, else reclaim the name. + _move(contracts, hasBid ? bidder : _reclaimAddress(packedInfo), packedInfo); + } else { + b.lastPrice = SafeCastLib.toUint88(newPrice); + b.lastUpdated = uint40(block.timestamp); + b.backing = uint88(F.rawSub(backing, spent)); + } + emit Poked(clusterName, sender); + } + + /// @dev Performs a bid on `clusterName`. + /// If the bidder is same as the previous bidder, the previous bid amount will be + /// incremented by the `msg.value`. + function bid(bytes32 clusterName) public payable nonReentrant { + (uint256 contracts, uint256 packedInfo, Bid storage b, address sender) = _registeredCtx(clusterName); + if (_owner(packedInfo) == sender) revert SelfBid(); + + (address oldBidder, uint256 oldBidAmount) = (b.bidder, b.bidAmount); + if (sender == oldBidder) { + // Workflow for increasing bid. + uint256 newBidAmount = F.rawAdd(oldBidAmount, msg.value); + b.bidUpdated = uint40(block.timestamp); + b.bidAmount = SafeCastLib.toUint88(newBidAmount); + _incrementTotalBidBacking(msg.value); + emit BidIncreased(clusterName, sender, oldBidAmount, newBidAmount); + _poke(contracts, packedInfo, clusterName, b, sender); + } else { + // Workflow for attempt to outbid. + uint256 thres = F.rawAdd(minBidIncrement(), oldBidAmount); + if (msg.value < F.max(_minAnnualPrice(contracts), thres)) revert Insufficient(); + b.bidUpdated = uint40(block.timestamp); + b.bidder = sender; + b.bidAmount = SafeCastLib.toUint88(msg.value); + _incrementTotalBidBacking(F.rawSub(msg.value, oldBidAmount)); + emit BidPlaced(clusterName, sender, msg.value); + // Poke decreases the total bid backing by the latest bid amount if name is moved. + _poke(contracts, packedInfo, clusterName, b, sender); + // Refund the previous bidder. + SafeTransferLib.forceSafeTransferETH(oldBidder, oldBidAmount); + emit BidRefunded(clusterName, oldBidder, oldBidAmount); + } + } + + /// @dev Reduces the bid on `clusterName` by `delta`. + /// If `delta` is equal to to greater than the current bid, revokes the bid. + /// Only the bidder can reduce their own bid, after `bidTimelock()` has passed. + function reduceBid(bytes32 clusterName, uint256 delta) public nonReentrant { + (uint256 contracts, uint256 packedInfo, Bid storage b, address sender) = _registeredCtx(clusterName); + if (block.timestamp < F.rawAdd(b.bidUpdated, bidTimelock())) revert BidTimelocked(); + if (b.bidder != sender) revert Unauthorized(); + + // Do a poke first, and if the poke does not move the name, perform the bid reduction. + if (!_poke(contracts, packedInfo, clusterName, b, sender)) { + uint256 oldBidAmount = b.bidAmount; + if (delta >= oldBidAmount) { + // If the delta is greater or equal to the old bid amount, + // clamp it to the old bid amount, then remove the bid. + b.bidUpdated = 0; + b.bidder = address(0); + b.bidAmount = 0; + _decrementTotalBidBacking(oldBidAmount); + SafeTransferLib.forceSafeTransferETH(sender, oldBidAmount); + emit BidRevoked(clusterName, sender, oldBidAmount); + } else { + // Otherwise, subtract `delta` from the old bid amount, and ensure that + // the new bid amount is not less than the minimum annual price. + uint256 newBidAmount = F.rawSub(oldBidAmount, delta); + if (newBidAmount < _minAnnualPrice(contracts)) revert Insufficient(); + b.bidAmount = uint88(newBidAmount); + b.bidUpdated = uint40(block.timestamp); + _decrementTotalBidBacking(delta); + SafeTransferLib.forceSafeTransferETH(sender, delta); + emit BidReduced(clusterName, sender, oldBidAmount, newBidAmount); + } + } + } + + /// @dev Accepts the bid for `clusterName`. + function acceptBid(bytes32 clusterName) public nonReentrant { + (uint256 contracts, uint256 packedInfo, Bid storage b, address sender) = _registeredCtx(clusterName); + (address bidder, uint256 bidAmount) = (b.bidder, b.bidAmount); + if (bidder == address(0)) revert NoBid(); + if (_owner(packedInfo) != sender) revert Unauthorized(); + + b.lastPrice = SafeCastLib.toUint88(_minAnnualPrice(contracts)); + b.lastUpdated = uint40(block.timestamp); + b.backing = uint88(bidAmount); + b.bidUpdated = 0; + b.bidder = address(0); + b.bidAmount = 0; + _decrementTotalBidBacking(bidAmount); + _move(contracts, bidder, packedInfo); + emit BidAccepted(clusterName, sender, bidder, bidAmount); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC VIEW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the consolidated info about the `clusterName`. + function nameInfo(bytes32 clusterName) public view returns (NameInfo memory info) { + ClustersMarketStorage storage $ = _getClustersMarketStorage(); + uint256 contracts = $.contracts; + uint256 packedInfo = _packedInfo(contracts, clusterName); + info.id = _id(packedInfo); + info.owner = _owner(packedInfo); + info.startTimestamp = _startTimestamp(packedInfo); + info.isRegistered = _isRegistered(packedInfo); + Bid memory b = $.bids[clusterName]; + info.lastPrice = b.lastPrice; + info.lastUpdated = b.lastUpdated; + info.bidAmount = b.bidAmount; + info.bidUpdated = b.bidUpdated; + info.bidder = b.bidder; + info.backing = b.backing; + if (info.lastUpdated == uint256(0)) { + (uint256 initialTimestamp, uint256 initialBacking) = _initialData(contracts, clusterName); + info.lastPrice = SafeCastLib.toUint88(_minAnnualPrice(contracts)); + info.lastUpdated = SafeCastLib.toUint40(initialTimestamp); + info.backing = SafeCastLib.toUint88(initialBacking); + } + } + + /// @dev Returns if the `clusterName` is registered. + function isRegistered(bytes32 clusterName) public view returns (bool) { + return _isRegistered(_packedInfo(_getClustersMarketStorage().contracts, clusterName)); + } + + /// @dev Returns the pricing contract. + function pricingContract() public view returns (address) { + return address(uint160((_getClustersMarketStorage().contracts << 128) >> 128)); + } + + /// @dev Returns the nft contract. + function nftContract() public view returns (address) { + return address(uint160(_getClustersMarketStorage().contracts >> 128)); + } + + /// @dev Returns the minimum bid increment. + function minBidIncrement() public view returns (uint256) { + return _getClustersMarketStorage().minBidIncrement; + } + + /// @dev Returns the bid timelock. + function bidTimelock() public view returns (uint256) { + return _getClustersMarketStorage().bidTimelock; + } + + /// @dev Returns the total amount of bid backing. + function totalBidBacking() public view returns (uint256) { + return _getClustersMarketStorage().totalBidBacking; + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ADMIN FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Allows the owner to set the pricing contract. + function setPricingContract(address newContract) public onlyOwner { + uint256 c = uint256(uint160(newContract)); + if (c != uint128(c)) revert ContractAddressOverflow(); + ClustersMarketStorage storage $ = _getClustersMarketStorage(); + $.contracts = (($.contracts >> 128) << 128) | c; + emit PricingContractSet(newContract); + } + + /// @dev Allows the owner to set the NFT contract. + function setNFTContract(address newContract) public onlyOwner { + uint256 c = uint256(uint160(newContract)); + if (c != uint128(c)) revert ContractAddressOverflow(); + ClustersMarketStorage storage $ = _getClustersMarketStorage(); + $.contracts = (($.contracts << 128) >> 128) | (c << 128); + emit NFTContractSet(newContract); + } + + /// @dev Allows the owner to set the minimum bid increment. + function setMinBidIncrement(uint256 newMinBidIncrement) public onlyOwner { + _getClustersMarketStorage().minBidIncrement = SafeCastLib.toUint88(newMinBidIncrement); + emit MinBidIncrementSet(newMinBidIncrement); + } + + /// @dev Allows the owner to set the bid timelock. + function setBidTimelock(uint256 newBidTimelock) public onlyOwner { + _getClustersMarketStorage().bidTimelock = SafeCastLib.toUint32(newBidTimelock); + emit BidTimelockSet(newBidTimelock); + } + + /// @dev Allows the owner to withdraw the protocol accrual. + function withdrawNative(address to, uint256 amount) public onlyOwner { + ClustersMarketStorage storage $ = _getClustersMarketStorage(); + uint256 withdrawable = F.zeroFloorSub(address(this).balance, $.totalBidBacking); + uint256 clampedAmount = F.min(amount, withdrawable); + SafeTransferLib.forceSafeTransferETH(to, clampedAmount); + emit NativeWithdrawn(to, clampedAmount); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* CONTRACT INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + // Note: + // - `packedInfo` is an uint256 that contains the NFT `id` along with it's owner. + // Bits Layout: + // - [0..39] `id`. + // - [40..79] `startTimestamp`. + // - [96..255] `owner`. + // - `contracts` is a uint256 that contains both the pricing contract and NFT contract. + // By passing around packed variables, we save gas on stack ops and avoid stack-too-deep. + + /// @dev Returns the packed info of `clusterName`. + function _packedInfo(uint256 contracts, bytes32 clusterName) internal view returns (uint256 result) { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(0x00, 0xffbda1c3) // `infoOf(bytes32)`. + mstore(0x20, clusterName) + let success := staticcall(gas(), shr(128, contracts), 0x1c, 0x24, m, 0x60) + if iszero(and(gt(returndatasize(), 0x5f), success)) { revert(codesize(), 0x00) } + // `infoOf` returns 3 words: `id`, `owner`, `startTimestamp`. + // - `id` : guaranteed to fit in 40 bits. + // - `owner` : guaranteed to fit in 160 bits. + // - `startTimestamp` : guaranteed to fit in 40 bits. + result := or(shl(96, mload(add(m, 0x20))), or(shl(40, mload(add(m, 0x40))), mload(m))) + } + } + + /// @dev Returns the `initialTimestamp` and `initialBacking` of `clusterName`. + function _initialData(uint256 contracts, bytes32 clusterName) + internal + view + returns (uint256 initialTimestamp, uint256 initialBacking) + { + assembly ("memory-safe") { + mstore(0x00, 0x4894cb4c) // `initialData(bytes32)`. + mstore(0x20, clusterName) + let success := staticcall(gas(), shr(128, contracts), 0x1c, 0x24, 0x00, 0x40) + if iszero(and(gt(returndatasize(), 0x3f), success)) { revert(codesize(), 0x00) } + initialTimestamp := mload(0x00) + initialBacking := mload(0x20) + } + } + + /// @dev Returns the owner of `packedInfo`. + function _owner(uint256 packedInfo) internal pure returns (address) { + return address(uint160(packedInfo >> 96)); + } + + /// @dev Returns the start timestamp of `packedInfo`. + function _startTimestamp(uint256 packedInfo) internal pure returns (uint256) { + return uint40(packedInfo >> 40); + } + + /// @dev Returns the id of `packedInfo`. + function _id(uint256 packedInfo) internal pure returns (uint256) { + return uint40(packedInfo); + } + + /// @dev Returns if the name has been registered. + function _isRegistered(uint256 packedInfo) internal pure returns (bool result) { + // Returns whether the owner is any address greater than `0..256`. + return packedInfo >> 96 > 0x100; + } + + /// @dev Returns the address which the name is to be reclaimed into. + /// This is to allow for a world where we have more than 4294967295 cluster names. + function _reclaimAddress(uint256 packedInfo) internal pure returns (address result) { + assembly ("memory-safe") { + result := add(1, and(0xff, packedInfo)) + } + } + + /// @dev Calls `mintNext` on the nft contract. + function _mintNext(uint256 contracts, bytes32 clusterName, address to) internal { + assembly ("memory-safe") { + mstore(0x00, 0x5dcdcf970000000000000000) // `mintNext(bytes32,address)`. + mstore(0x18, clusterName) + mstore(0x38, shr(96, shl(96, to))) + if iszero(call(gas(), shr(128, contracts), 0, 0x14, 0x44, codesize(), 0x00)) { + returndatacopy(mload(0x40), 0x00, returndatasize()) + revert(mload(0x40), returndatasize()) // Bubble up the revert. + } + mstore(0x38, 0) // Restore the part of the free memory pointer that was overwritten. + } + } + + /// @dev Calls `conduitSafeTransfer` on the nft contract. + function _move(uint256 contracts, address to, uint256 packedInfo) internal { + assembly ("memory-safe") { + let m := mload(0x40) + mstore(m, 0x7ac99264) // `conduitSafeTransfer(address,address,uint256)`. + mstore(add(m, 0x20), shr(96, packedInfo)) // `from`. + mstore(add(m, 0x40), shr(96, shl(96, to))) // `to`. + mstore(add(m, 0x60), and(0xffffffffff, packedInfo)) // `id`. + if iszero(call(gas(), shr(128, contracts), 0, add(m, 0x1c), 0x64, codesize(), 0x00)) { + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) // Bubble up the revert. + } + } + } + + /// @dev Returns the minimum annual price. + function _minAnnualPrice(uint256 contracts) internal view returns (uint256 result) { + assembly ("memory-safe") { + mstore(0x00, 0x360c93dd) // `minAnnualPrice()`. + let success := staticcall(gas(), shr(128, shl(128, contracts)), 0x1c, 0x04, 0x00, 0x20) + if iszero(and(gt(returndatasize(), 0x1f), success)) { revert(codesize(), 0x00) } + result := mload(0x00) + } + } + + /// @dev Returns the integrated price. + function _getIntegratedPrice(uint256 contracts, uint256 lastUpdatedPrice, uint256 secondsSinceUpdate) + internal + view + returns (uint256 spent, uint256 price) + { + assembly ("memory-safe") { + mstore(0x00, 0x4e34478d0000000000000000) // `getIntegratedPrice(uint256,uint256)`. + mstore(0x18, lastUpdatedPrice) + mstore(0x38, secondsSinceUpdate) + let success := staticcall(gas(), shr(128, shl(128, contracts)), 0x14, 0x44, 0x00, 0x40) + if iszero(and(gt(returndatasize(), 0x3f), success)) { revert(codesize(), 0x00) } + spent := mload(0x00) + price := mload(0x20) + mstore(0x38, 0) // Restore the part of the free memory pointer that was overwritten. + } + } + + /// @dev Helper for returning the context variables for a registered `clusterName`. + function _registeredCtx(bytes32 clusterName) + internal + returns (uint256 contracts, uint256 packedInfo, Bid storage b, address sender) + { + contracts = _getClustersMarketStorage().contracts; + packedInfo = _packedInfo(contracts, clusterName); + if (!_isRegistered(packedInfo)) revert NameNotRegistered(); + b = _getClustersMarketStorage().bids[clusterName]; + if (b.lastUpdated == uint256(0)) { + (uint256 initialTimestamp, uint256 initialBacking) = _initialData(contracts, clusterName); + b.lastPrice = SafeCastLib.toUint88(_minAnnualPrice(contracts)); + b.lastUpdated = SafeCastLib.toUint40(initialTimestamp); + b.backing = SafeCastLib.toUint88(initialBacking); + } + sender = MessageHubLib.senderOrSigner(); + } + + /// @dev Increments the total bid backing. + function _incrementTotalBidBacking(uint256 amount) internal { + ClustersMarketStorage storage $ = _getClustersMarketStorage(); + $.totalBidBacking = SafeCastLib.toUint88(uint256($.totalBidBacking) + amount); + } + + /// @dev Decrements the total bid backing. + function _decrementTotalBidBacking(uint256 amount) internal { + ClustersMarketStorage storage $ = _getClustersMarketStorage(); + $.totalBidBacking = uint88(uint256($.totalBidBacking) - amount); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev For UUPS upgradeability. + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} +} diff --git a/src/ClustersNFTBaseURIRenderer.sol b/src/ClustersNFTBaseURIRenderer.sol new file mode 100644 index 0000000..5281ad4 --- /dev/null +++ b/src/ClustersNFTBaseURIRenderer.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {LibString} from "solady/utils/LibString.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; + +/// @title ClustersNFTBaseURIRenderer +/// @dev A very simple contract to return the token URI for the Clusters NFT, +/// using a base URI and the token ID. +contract ClustersNFTBaseURIRenderer is Ownable { + using LibString for LibString.StringStorage; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + LibString.StringStorage internal _baseURI; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* EVENTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The base URI is updated. + event BaseURISet(string baseURI); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* CONSTRUCTOR */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + constructor() { + _initializeOwner(tx.origin); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* TOKEN URI */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the token URI for the given `id`. + function tokenURI(uint256 id) public view returns (string memory) { + return LibString.replace(_baseURI.get(), "{id}", LibString.toString(id)); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ADMIN FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Sets the base URI for the token. + function setBaseURI(string memory baseURI) public onlyOwner { + _baseURI.set(baseURI); + emit BaseURISet(baseURI); + } +} diff --git a/src/ClustersNFTV1.sol b/src/ClustersNFTV1.sol new file mode 100644 index 0000000..f6edf20 --- /dev/null +++ b/src/ClustersNFTV1.sol @@ -0,0 +1,551 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; +import {Initializable} from "solady/utils/Initializable.sol"; +import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol"; +import {LibMap} from "solady/utils/LibMap.sol"; +import {LibBit} from "solady/utils/LibBit.sol"; +import {LibString} from "solady/utils/LibString.sol"; +import {Ownable} from "solady/auth/Ownable.sol"; +import {ERC721} from "solady/tokens/ERC721.sol"; +import {EnumerableRoles} from "solady/auth/EnumerableRoles.sol"; +import {MessageHubLibV1 as MessageHubLib} from "clusters/MessageHubLibV1.sol"; + +/// @title ClustersNFTV1 +/// @notice Each name is represented by a unique NFT. +/// Once a name is minted, it cannot be changed. +/// For simplicity, this contract does not support burning. +contract ClustersNFTV1 is UUPSUpgradeable, Initializable, ERC721, Ownable, EnumerableRoles { + using LibMap for LibMap.Uint40Map; + using DynamicArrayLib for uint256[]; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STRUCTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev A struct to hold the mint information for `mintNext`. + struct Mint { + // The name to be minted. + bytes32 clusterName; + // The mint recipient. + address to; + // The initial timestamp for the market pricing integral. + uint256 initialTimestamp; + // The initial backing for the market. + uint256 initialBacking; + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* CONSTANTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Admin. + uint256 public constant ADMIN_ROLE = 0; + + /// @dev Minter. + uint256 public constant MINTER_ROLE = 1; + + /// @dev For marketplaces to transfer tokens. + uint256 public constant CONDUIT_ROLE = 2; + + /// @dev The maximum role. + uint256 public constant MAX_ROLE = 2; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + // Note: Solady's ERC721 has auxiliary data. + // + // - `_getExtraData(uint256 id) internal view virtual returns (uint96 result)`. + // For names of 1 to 12 bytes, we will store it in the per-token `extraData`. + // Otherwise, we will store it in the `fullName` mapping. + // + // - `_getAux(address owner) internal view virtual returns (uint224 result)`. + // We will use this 224 bits to store the default name id, as well as + // the ids of the tokens `0,1,2,3,4` of the owner. + + /// @dev The storage struct for a single name. + struct NameData { + // Bits Layout: + // - [0..39] `id`. + // - [40..47] `ownedIndex`. + // - [48..87] `startTimestamp`. + // - [88..255] `additionalData`. + uint256 packed; + // Stores the owned index if it is greater than 254. + uint256 fullOwnedIndex; + // The initial packed data. + uint256 initialPacked; + } + + /// @dev The storage struct for the contract. + struct ClustersNFTStorage { + // Auto-incremented and used for assigning the NFT `id`, starting from 1. + uint256 totalMinted; + // Mapping of NFT `id` to the full `name`. + mapping(uint256 => bytes32) fullName; + // Mapping of NFT `owner` to their full default NFT `id`. + mapping(address => uint256) fullDefaultId; + // Mapping of NFT `owner` to the NFT `id`, from 5 onwards. + mapping(address => LibMap.Uint40Map) ownedIds; + // Mapping of `name` to the `NameData`. + mapping(bytes32 => NameData) nameData; + // Whether transfers, excluding mints, are paused. For bulk seeding phase. + bool paused; + // Contract for rendering the token URI. + address tokenURIRenderer; + } + + /// @dev Returns the storage struct for the contract. + function _getClustersNFTStorage() internal pure returns (ClustersNFTStorage storage $) { + assembly ("memory-safe") { + // `uint72(bytes9(keccak256("Clusters.ClustersNFTStorage")))`. + $.slot := 0xda8b89020ecb842518 // Truncate to 9 bytes to reduce bytecode size. + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* EVENTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The token URI renderer is set. + event TokenURIRendererSet(address renderer); + + /// @dev The default id of `owner` has been set to `id`. + event DefaultIdSet(address indexed owner, uint256 id); + + /// @dev The paused status of the contract is updated. + event PausedSet(bool paused); + + /// @dev The linked address of `clusterName` is updated. + event LinkedAddressSet(bytes32 indexed clusterName, address indexed linkedAddress); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERRORS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The name cannot be `bytes32(0)`. + error NameIsZero(); + + /// @dev The name already exists. + error NameAlreadyExists(); + + /// @dev The query index is out of bounds. + error IndexOutOfBounds(); + + /// @dev Cannot mint nothing. + error NothingToMint(); + + /// @dev Transfers are paused. + error Paused(); + + /// @dev The name is invalid. + error InvalidName(); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INITIALIZER */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Initializes the contract, seting the initial owner. + function initialize(address initialOwner) public initializer onlyProxy { + _initializeOwner(initialOwner); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* META */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the name and version of the contract. + function contactNameAndVersion() public pure returns (string memory, string memory) { + return (name(), "1.0.0"); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERC721 METADATA */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the name of the contract. + function name() public pure override returns (string memory) { + return "Clusters"; + } + + /// @dev Returns the symbol of the contract. + function symbol() public pure override returns (string memory) { + return "CLUSTERS"; + } + + /// @dev Returns the token URI for the given `id`. + /// This method may perform a direct return via inline assembly + /// for efficiency, and thus must not be called internally. + function tokenURI(uint256 id) public view override returns (string memory) { + if (!_exists(id)) revert TokenDoesNotExist(); + address renderer = _getClustersNFTStorage().tokenURIRenderer; + if (renderer == address(0)) return ""; + assembly ("memory-safe") { + let m := mload(0x40) + mstore(0x00, 0xc87b56dd) // `tokenURI(uint256)`. + mstore(0x20, id) + if iszero(staticcall(gas(), renderer, 0x1c, 0x24, 0x00, 0x00)) { + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) + } + returndatacopy(m, 0x00, returndatasize()) + return(m, returndatasize()) + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERC721 ENUMERABLE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the total number of NFTs. + function totalSupply() public view returns (uint256) { + return _getClustersNFTStorage().totalMinted; + } + + /// @dev Returns the token ID by the given `index`. + function tokenByIndex(uint256 index) public view returns (uint256) { + if (index >= totalSupply()) revert IndexOutOfBounds(); + unchecked { + return index + 1; + } + } + + /// @dev Returns the token IDs of `owner`. + /// This method may exceed the block gas limit if the owner has too many tokens. + /// This method performs a direct return via inline assembly for efficiency, + /// and is thus marked as external. + function tokensOfOwner(address owner) external view returns (uint256[] memory) { + uint256 n = balanceOf(owner); + uint256[] memory result = DynamicArrayLib.malloc(n); + unchecked { + for (uint256 i; i != n; ++i) { + result.set(i, _getId(owner, i)); + } + } + DynamicArrayLib.directReturn(result); + } + + /// @dev Returns the token ID of `owner` at `i`. + function tokenOfOwnerByIndex(address owner, uint256 i) public view returns (uint256) { + if (i > balanceOf(owner)) revert IndexOutOfBounds(); + return _getId(owner, i); + } + + /// @dev ERC-165 override. + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + // 0x780e9d63 is the interface ID for ERC721Enumerable. + return LibBit.or(super.supportsInterface(interfaceId), interfaceId == 0x780e9d63); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC VIEW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the name of token `id`. + /// Returns zero if it does not exist. + function nameOf(uint256 id) public view returns (bytes32) { + uint96 truncatedName = _getExtraData(id); + if (truncatedName != 0) return bytes12(truncatedName); + return _getClustersNFTStorage().fullName[id]; + } + + /// @dev Returns the token `id`, `owner`, and `startTimestamp` of `clusterName`. + /// All values are zero if it does not exist. + /// Used by the Market. + function infoOf(bytes32 clusterName) public view returns (uint256 id, address owner, uint256 startTimestamp) { + uint256 p = _getClustersNFTStorage().nameData[clusterName].packed; + id = uint40(p); + owner = _ownerOf(id); + startTimestamp = uint40(p >> 48); + } + + /// @dev Returns the token URI renderer contract. + function tokenURIRenderer() public view returns (address) { + return _getClustersNFTStorage().tokenURIRenderer; + } + + /// @dev Returns whether token transfers are paused. + function isPaused() public view returns (bool) { + return _getClustersNFTStorage().paused; + } + + /// @dev Returns the initial additional data of `clusterName`. + function initialData(bytes32 clusterName) public view returns (uint256 initialTimestamp, uint256 initialBacking) { + NameData memory d = _getClustersNFTStorage().nameData[clusterName]; + uint256 p = d.packed; + if (p >> 248 != uint256(0)) p = d.initialPacked; + initialBacking = p >> (88 + 40); + initialTimestamp = uint40(p >> 88); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* LINKED ADDRESS FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Sets the additional data of `clusterName`. + /// Does not revert even if `clusterName` does not exist, + /// so that we can have the freedom to access before the name exists. + function setLinkedAddress(bytes32 clusterName, address newLinkedAddress) public { + NameData storage d = _getClustersNFTStorage().nameData[clusterName]; + uint256 p = d.packed; + if (MessageHubLib.senderOrSigner() != _ownerOf(uint40(p))) revert Unauthorized(); + if (p >> 248 == uint256(0)) d.initialPacked = p; + assembly ("memory-safe") { + mstore(0x0b, p) + mstore(0x00, newLinkedAddress) + mstore8(0x0b, 1) + sstore(d.slot, mload(0x0b)) + } + emit LinkedAddressSet(clusterName, newLinkedAddress); + } + + /// @dev Returns the linked address of `clusterName`. + function linkedAddress(bytes32 clusterName) public view returns (address) { + NameData memory d = _getClustersNFTStorage().nameData[clusterName]; + uint256 p = d.packed; + return p >> 248 == uint256(0) ? address(0) : address(uint160(p >> 88)); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* MINTING */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Mints a new NFT with the given `clusterName` and assigns it to `to`. + /// Used by the Market. + function mintNext(bytes32 clusterName, address to) public onlyOwnerOrRole(MINTER_ROLE) returns (uint256 id) { + id = _mintNext(clusterName, to, 0, 0); + } + + /// @dev Mints new NFTs with the given `clusterNames` and assigns them to `to`. + function mintNext(Mint[] calldata mints) public onlyOwnerOrRole(MINTER_ROLE) returns (uint256 startId) { + if (mints.length == uint256(0)) revert NothingToMint(); + unchecked { + for (uint256 i; i != mints.length; ++i) { + Mint calldata mint; + assembly ("memory-safe") { + mint := add(mints.offset, shl(7, i)) // `mul(0x80, i)`. + } + uint256 id = _mintNext(mint.clusterName, mint.to, mint.initialTimestamp, mint.initialBacking); + if (i == uint256(0)) startId = id; + } + } + } + + /// @dev Mints a new NFT with the given `clusterName` and assigns it to `to`. + function _mintNext(bytes32 clusterName, address to, uint256 initialTimestamp, uint256 initialBacking) + internal + returns (uint256 id) + { + if (!_isValidName(clusterName)) revert InvalidName(); + ClustersNFTStorage storage $ = _getClustersNFTStorage(); + + NameData storage d = $.nameData[clusterName]; + if (uint40(d.packed) != 0) revert NameAlreadyExists(); + + unchecked { + id = ++$.totalMinted; + } + uint96 truncatedName = uint96(bytes12(clusterName)); + if (bytes12(truncatedName) != clusterName) { + $.fullName[id] = clusterName; + truncatedName = 0; + } + + require((((id | initialTimestamp) >> 40) | (initialBacking >> 88)) == uint256(0)); + + // Construct the new `p` with `id`, the timestamp, with `additionalData` + // initialized with `initialTimestamp` and `initialBacking`. + uint256 p = (((initialBacking << 40) | initialTimestamp) << 88) | id | (block.timestamp << 48); + + uint256 ownedIndex = balanceOf(to); + if (ownedIndex >= 0xff) { + d.packed = p; + d.fullOwnedIndex = ownedIndex; + } else { + d.packed = _setByte(p, 26, 0xff ^ ownedIndex); + } + _setId(to, ownedIndex, uint40(id)); + + _mintAndSetExtraDataUnchecked(to, id, truncatedName); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERC721 OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev This affects the `safeTransferFrom` variants too. + function transferFrom(address from, address to, uint256 id) public payable override { + _transfer(MessageHubLib.senderOrSigner(), from, to, id); + } + + /// @dev Override for `approve`. + function approve(address account, uint256 id) public payable override { + _approve(MessageHubLib.senderOrSigner(), account, id); + } + + /// @dev Override for `setApprovalForAll`. + function setApprovalForAll(address operator, bool isApproved) public override { + _setApprovalForAll(MessageHubLib.senderOrSigner(), operator, isApproved); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* DEFAULT ID FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Sets the default token ID of the caller. + /// Does not revert if the token ID is not actually owned by the caller. + function setDefaultId(uint256 id) public { + address sender = MessageHubLib.senderOrSigner(); + uint224 aux = (_getAux(sender) >> 24) << 24; + if (id >= 0xffffff) { + _setAux(sender, aux); + _getClustersNFTStorage().fullDefaultId[sender] = id; + } else { + _setAux(sender, aux | uint224(id)); + } + emit DefaultIdSet(sender, id); + } + + /// @dev Returns the default id of `owner`. + /// If the owner does not own their default id, returns one of the ids owned by `owner`. + /// If the owner does not have any tokens, returns `0`. + function defaultId(address owner) public view returns (uint256) { + if (balanceOf(owner) == 0) return 0; + uint256 result = _getAux(owner) & 0xffffff; + if (result == uint256(0)) result = _getClustersNFTStorage().fullDefaultId[owner]; + if (result != 0) if (_ownerOf(result) == owner) return result; + return _getId(owner, 0); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ADMIN FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Sets the token URI renderer contract. + function setTokenURIRenderer(address renderer) public onlyOwnerOrRole(ADMIN_ROLE) { + _getClustersNFTStorage().tokenURIRenderer = renderer; + emit TokenURIRendererSet(renderer); + } + + /// @dev Sets the paused status of the contract. + function setPaused(bool paused) public onlyOwnerOrRole(ADMIN_ROLE) { + _getClustersNFTStorage().paused = paused; + emit PausedSet(paused); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* CONDUIT TRANSFER */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Enables the marketplace to transfer the NFT. This is used when a NFT is outbidded. + /// Used by the Market. + function conduitSafeTransfer(address from, address to, uint256 id) public onlyRole(CONDUIT_ROLE) { + _safeTransfer(from, to, id); // We don't need data, since this NFT doesn't use it. + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Sets the `i`-th byte of `x` to `b` and returns the result. + function _setByte(uint256 x, uint256 i, uint256 b) internal pure returns (uint256 result) { + assembly ("memory-safe") { + mstore(0x00, x) + mstore8(i, b) + result := mload(0x00) + } + } + + /// @dev Returns the token of `owner` at index `i`. + function _getId(address owner, uint256 i) internal view returns (uint40) { + unchecked { + if (i >= 5) { + return _getClustersNFTStorage().ownedIds[owner].get(i - 5); + } else { + return uint40(_getAux(owner) >> (24 + i * 40)); + } + } + } + + /// @dev Sets the token of `owner` at index `i`. + function _setId(address owner, uint256 i, uint40 id) internal { + unchecked { + if (i >= 5) { + _getClustersNFTStorage().ownedIds[owner].set(i - 5, id); + } else { + uint224 aux = _getAux(owner); + uint256 o = 24 + i * 40; + assembly ("memory-safe") { + aux := xor(aux, shl(o, and(0xffffffffff, xor(shr(o, aux), id)))) + } + _setAux(owner, aux); + } + } + } + + /// @dev Returns if the name is a valid cluster name. + function _isValidName(bytes32 clusterName) internal pure returns (bool result) { + uint256 m; + assembly ("memory-safe") { + m := mload(0x40) // Cache the free memory pointer. + } + string memory s = LibString.fromSmallString(clusterName); + bool allValidChars = LibString.is7BitASCII(s, 0x7fffffe8000000003ff200000000000); // `[a-z0-9_-]+`. + assembly ("memory-safe") { + let notNormalized := xor(mload(add(0x20, s)), clusterName) + result := iszero(or(or(iszero(allValidChars), iszero(clusterName)), notNormalized)) + mstore(0x40, m) // Restore the free memory pointer. + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Used for maintaining the enumeration of owned tokens. + function _afterTokenTransfer(address from, address to, uint256 id) internal override { + // If it's a mint, or self-transfer, early return. + // We don't need to care about the burn case. + if (LibBit.or(from == address(0), from == to)) return; + + ClustersNFTStorage storage $ = _getClustersNFTStorage(); + if ($.paused) revert Paused(); + NameData storage d = $.nameData[nameOf(id)]; + uint256 p = d.packed; + // Update the timestamp in `p`. + p ^= (((p >> 48) ^ block.timestamp) & 0xffffffffff) << 48; + + // Remove token from owner enumeration. + uint256 j = balanceOf(from); // The last token index. + uint256 i = uint8(bytes32(p)[26]); + i = i == uint256(0) ? d.fullOwnedIndex : 0xff ^ i; // Current token index. + if (i != j) { + uint40 lastTokenId = _getId(from, j); + _setId(from, i, lastTokenId); + NameData storage e = $.nameData[nameOf(lastTokenId)]; + if (i >= 0xff) { + e.packed = _setByte(e.packed, 26, 0); + e.fullOwnedIndex = i; + } else { + e.packed = _setByte(e.packed, 26, 0xff ^ i); + } + } + // Add token to owner enumeration. + unchecked { + i = balanceOf(to) - 1; // The new owned index of `id`. + _setId(to, i, uint40(id)); + if (i >= 0xff) { + d.packed = _setByte(p, 26, 0); + d.fullOwnedIndex = i; + } else { + d.packed = _setByte(p, 26, 0xff ^ i); + } + } + } + + /// @dev For UUPS upgradeability. + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} +} diff --git a/src/MessageHubLibV1.sol b/src/MessageHubLibV1.sol new file mode 100644 index 0000000..b696991 --- /dev/null +++ b/src/MessageHubLibV1.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {LibMulticaller} from "multicaller/LibMulticaller.sol"; + +/// @title MessageHubLibV1 +/// @notice Library for returning the `senderOrSigner`. +library MessageHubLibV1 { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* CONSTANTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The canonical address of the address hub. + address internal constant MESSAGE_HUB = address(123456); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OPERATIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the sender or signer. + /// Note: this function will never return a zero address. + function senderOrSigner() internal view returns (address result) { + result = LibMulticaller.senderOrSigner(); + if (result == MESSAGE_HUB) { + assembly ("memory-safe") { + if and(gt(returndatasize(), 0x1f), staticcall(gas(), result, 0x00, 0x00, 0x00, 0x20)) { + result := mload(0x00) + } + } + } + } +} diff --git a/src/MessageHubV1.sol b/src/MessageHubV1.sol new file mode 100644 index 0000000..0eade5f --- /dev/null +++ b/src/MessageHubV1.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {LibBytes} from "solady/utils/LibBytes.sol"; +import {ERC7821} from "solady/accounts/ERC7821.sol"; +import {Origin, OAppReceiverUpgradeable} from "layerzero-oapp/contracts/oapp-upgradeable/OAppReceiverUpgradeable.sol"; + +/// @title MessageHubPodV1 +/// @notice A hyper minimal smart account that is controlled by the MessageHubV1 +/// This is used when the message comes from a non-Ethereum chain. +contract MessageHubPodV1 is ERC7821 { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev For ERC7821. + function _execute(bytes32, bytes calldata, Call[] calldata calls, bytes calldata) internal virtual override { + bytes memory args = LibClone.argsOnClone(address(this), 0x00, 0x34); + assembly ("memory-safe") { + let requiredCaller := shr(96, mload(add(args, 0x40))) // `mothership`. + let requiredHash := mload(add(args, 0x20)) + let m := mload(0x40) // Cache the free memory pointer. + if iszero( + and( // All arguments are evaluated from last to first. + and( + // `keccak256(abi.encode(originalSender, originalSenderType)) == requiredHash`. + eq(keccak256(0x20, 0x40), requiredHash), + and( + eq(returndatasize(), 0x60), // `mothership` returns `0x60` bytes. + eq(caller(), requiredCaller) // The caller is the `mothership`. + ) + ), + staticcall(gas(), requiredCaller, 0x00, 0x00, 0x00, 0x60) + ) + ) { + mstore(0x00, 0x82b42900) // `Unauthorized()`. + revert(0x1c, 0x04) + } + mstore(0x40, m) // Restore the free memory pointer. + } + _execute(calls, bytes32(0)); + } +} + +/// @title MessageHubV1 +/// @notice Generalized message hub for LayerZero. +contract MessageHubV1 is UUPSUpgradeable, OAppReceiverUpgradeable { + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* TRANSIENT STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The sender transient storage slot. + uint256 internal constant _SENDER_TRANSIENT_SLOT = 0; + + /// @dev The original sender transient storage slot. + uint256 internal constant _ORIGINAL_SENDER_TRANSIENT_SLOT = 1; + + /// @dev The original sender type transient storage slot. + uint160 internal constant _ORIGINAL_SENDER_TYPE_TRANSIENT_SLOT = 2; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ERRORS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Reentrancy not supported. + error Reentrancy(); + + /// @dev Unauthorized access. + error Unauthorized(); + + /// @dev Original sender is zero. + error OriginalSenderIsZero(); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* IMMUTABLES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The address of the pod implementation. + address internal immutable _podImplementation = address(new MessageHubPodV1()); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INITIALIZER */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Initializes the contract. + function initialize(address endpoint_, address owner_) public initializer onlyProxy { + _initializeOAppCore(endpoint_, owner_); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* META */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the name and version of the contract. + function contractNameAndVersion() public pure returns (string memory, string memory) { + return ("MessageHub", "1.0.0"); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC VIEW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the deterministic address of the sub account. + function predictSubAccount(bytes32 originalSender, uint256 originalSenderType) public view returns (address) { + bytes memory args = _subAccountArgs(originalSender, originalSenderType); + return LibClone.predictDeterministicAddress(_podImplementation, args, keccak256(args), address(this)); + } + + /// @dev Returns `abi.encode(sender, originalSender, originalSenderType)`. + /// This is queried by the MessageHubPodV1 or contracts that use MessageHubLibV1. + receive() external payable { + assembly ("memory-safe") { + // The `sender` can be either: + // - A MessageHubPodV1 that is deployed on-the-fly if the + // message was initiated from a non-Ethereum address. + // - An Ethereum address that initiated the message. + // + // The `originalSender` can be either: + // - A non-Ethereum address. + // - An Ethereum address that matches `sender`. + mstore(0x00, tload(_SENDER_TRANSIENT_SLOT)) + mstore(0x20, tload(_ORIGINAL_SENDER_TRANSIENT_SLOT)) + mstore(0x40, tload(_ORIGINAL_SENDER_TYPE_TRANSIENT_SLOT)) + return(0x00, 0x60) + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ADMIN FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Withdraws the native token. + function withdrawNative(address to, uint256 amount) public { + if (msg.sender != owner()) revert Unauthorized(); + SafeTransferLib.safeTransferETH(to, amount); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INTERNAL HELPERS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Creates a sub account. + function _createSubAccount(bytes32 originalSender, uint256 originalSenderType) + internal + returns (address instance) + { + bytes memory args = _subAccountArgs(originalSender, originalSenderType); + (, instance) = LibClone.createDeterministicClone(_podImplementation, args, keccak256(args)); + } + + /// @dev Returns the immutable arguments for the sub account. + /// `abi.encodePacked(keccak256(abi.encode(originalSender, originalSenderType)), address(this))`. + function _subAccountArgs(bytes32 originalSender, uint256 originalSenderType) + internal + view + returns (bytes memory result) + { + if (originalSender == bytes32(0)) revert OriginalSenderIsZero(); + assembly ("memory-safe") { + result := mload(0x40) + mstore(0x00, originalSender) + mstore(0x20, originalSenderType) + // Address of the mothership. + mstore(add(result, 0x34), address()) + // Hash of the original sender and sender type. + mstore(add(result, 0x20), keccak256(0x00, 0x40)) + mstore(result, 0x34) // Store the byte length of the arguments. + mstore(0x40, add(result, 0x54)) // Allocate memory. + } + } + + /// @dev Decodes and forwards the message to the target. + /// This modifier is to be attached onto the `_lzReceive` function. + modifier forwardMessage(bytes calldata message) { + bytes32 originalSender; + uint256 originalSenderType; + address target; + bytes calldata data; + + assembly ("memory-safe") { + // This is equivalent to + // `abi.decode(message, (bytes32, uint256, address, bytes))`. + // This is optimizoored as the hub is on L1. + originalSender := calldataload(add(message.offset, 0x00)) + originalSenderType := calldataload(add(message.offset, 0x20)) + // Don't need to clean bits, as `call` is agnostic to dirty upper 96 bits. + target := calldataload(add(message.offset, 0x40)) + let o := add(message.offset, calldataload(add(message.offset, 0x60))) + data.length := calldataload(o) + data.offset := add(o, 0x20) + // Check that all of the data is within bounds. + if or(lt(message.length, 0x80), gt(add(data.offset, data.length), add(message.offset, message.length))) { + invalid() + } + } + + if (originalSender == bytes32(0)) revert OriginalSenderIsZero(); + + address sender = address(uint160(uint256(originalSender))); + if (originalSenderType != 0) { + sender = _createSubAccount(originalSender, originalSenderType); + } + + uint256 toRefund; + assembly ("memory-safe") { + let balanceBefore := sub(selfbalance(), callvalue()) + // Disallow reentrancy. + if tload(_ORIGINAL_SENDER_TRANSIENT_SLOT) { + mstore(0x00, 0xab143c06) // `Reentrancy()`. + revert(0x1c, 0x04) + } + // Temporarily store the sender and original sender in transient storage. + tstore(_SENDER_TRANSIENT_SLOT, shr(96, shl(96, sender))) + tstore(_ORIGINAL_SENDER_TRANSIENT_SLOT, originalSender) + tstore(_ORIGINAL_SENDER_TYPE_TRANSIENT_SLOT, originalSenderType) + + let m := mload(0x40) + calldatacopy(m, data.offset, data.length) + // Calls `target` with `data` and `value`. + if iszero(call(gas(), target, callvalue(), m, data.length, 0x00, 0x00)) { + // Bubble up the revert if the call fails. + returndatacopy(mload(0x40), 0x00, returndatasize()) + revert(mload(0x40), returndatasize()) + } + // Reset transient storage. + tstore(_ORIGINAL_SENDER_TYPE_TRANSIENT_SLOT, 0) + tstore(_ORIGINAL_SENDER_TRANSIENT_SLOT, 0) + tstore(_SENDER_TRANSIENT_SLOT, 0) + + toRefund := mul(sub(selfbalance(), balanceBefore), gt(selfbalance(), balanceBefore)) + } + // If the balance has somehow increased, refunds to the sender. + if (toRefund != 0) { + SafeTransferLib.forceSafeTransferETH(sender, toRefund); + } + _; + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Receives a message from the OApp. + function _lzReceive( + Origin calldata, /* origin */ + bytes32, /* guid */ + bytes calldata message, + address, /* executor */ + bytes calldata /* extraData */ + ) internal override forwardMessage(message) {} + + /// @dev For UUPSUpgradeable. Only the owner can upgrade. + function _authorizeUpgrade(address) internal virtual override { + if (msg.sender != owner()) revert Unauthorized(); + } +} diff --git a/src/MessageInitiatorV1.sol b/src/MessageInitiatorV1.sol new file mode 100644 index 0000000..5d13707 --- /dev/null +++ b/src/MessageInitiatorV1.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol"; +import {OptionsBuilder} from "layerzero-oapp/contracts/oapp/libs/OptionsBuilder.sol"; +import { + OAppSenderUpgradeable, MessagingFee +} from "layerzero-oapp/contracts/oapp-upgradeable/OAppSenderUpgradeable.sol"; + +/// @title MessageInitiatorV1 +/// @notice Generalized contract for sending messages to `MessageHubV1`. +contract MessageInitiatorV1 is UUPSUpgradeable, OAppSenderUpgradeable { + using OptionsBuilder for bytes; + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* STORAGE */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev The storage struct for the contract. + struct MessageInitiatorStorage { + uint32 dstEid; + } + + /// @dev Returns the storage struct for the contract. + function _getMessageInitiatorStorage() internal pure returns (MessageInitiatorStorage storage $) { + assembly ("memory-safe") { + // `uint72(bytes9(keccak256("Clusters.MessageInitiatorStorage")))`. + $.slot := 0xf707b7e13707881ad4 // Truncate to 9 bytes to reduce bytecode size. + } + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* EVENTS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Emitted when the destination endpoint ID is set. + event DstEidSet(uint32 eid); + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* INITIALIZER */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Initializes the contract. + function initialize(address endpoint_, address owner_) public initializer onlyProxy { + _initializeOAppCore(endpoint_, owner_); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC WRITE FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Sends a message. + function sendWithDefaultOptions(address target, bytes memory data, uint256 gas, uint256 value) public payable { + send(target, data, defaultOptions(gas, value)); + } + + /// @dev Sends a message. + function send(address target, bytes memory data, bytes memory options) public payable { + bytes memory encoded = abi.encode(msg.sender, 0, target, data); + _lzSend(dstEid(), encoded, options, MessagingFee(msg.value, 0), payable(msg.sender)); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* PUBLIC VIEW FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Returns the Layerzero destination endpoint ID. + function dstEid() public view returns (uint32) { + return _getMessageInitiatorStorage().dstEid; + } + + /// @dev Returns the native fee for sending a message. + function quoteWithDefaultOptions(address target, bytes memory data, uint256 gas, uint256 value) + public + view + returns (uint256) + { + return quote(target, data, defaultOptions(gas, value)); + } + + /// @dev Returns the native fee for sending a message. + function quote(address target, bytes memory data, bytes memory options) public view returns (uint256) { + bytes memory encoded = abi.encode(keccak256(abi.encode(address(this))), 0, target, data); + return _quote(dstEid(), encoded, options, false).nativeFee; + } + + /// @dev Returns the default options that encodes `gas` and `value`. + function defaultOptions(uint256 gas, uint256 value) public pure returns (bytes memory) { + if (gas | value >= 1 << 128) revert(); + return OptionsBuilder.newOptions().addExecutorLzReceiveOption(uint128(gas), uint128(value)); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* ADMIN FUNCTIONS */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev Enables the owner to set the destination endpoint ID. + function setDstEid(uint32 eid) public onlyOwner { + _getMessageInitiatorStorage().dstEid = eid; + emit DstEidSet(eid); + } + + /*«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-«-*/ + /* OVERRIDES */ + /*-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»-»*/ + + /// @dev For UUPSUpgradeable. Only the owner can upgrade. + function _authorizeUpgrade(address) internal virtual override onlyOwner {} +} diff --git a/src/NameManagerHub.sol b/src/NameManagerHub.sol index e56353d..e31a243 100644 --- a/src/NameManagerHub.sol +++ b/src/NameManagerHub.sol @@ -27,34 +27,37 @@ abstract contract NameManagerHub is IClustersHub { IPricing internal pricing; /// @notice Which cluster an address belongs to - mapping(bytes32 addr => uint256 clusterId) public addressToClusterId; + mapping(bytes32 addr => uint256 clusterId) public addressToClusterId; // V1: Not needed. Moved to NFT. /// @notice Which cluster a name belongs to - mapping(bytes32 name => uint256 clusterId) public nameToClusterId; + mapping(bytes32 name => uint256 clusterId) public nameToClusterId; // V1: Not needed. Moved to NFT. /// @notice Display name to be shown for a cluster, like ENS reverse records - mapping(uint256 clusterId => bytes32 name) public defaultClusterName; + mapping(uint256 clusterId => bytes32 name) public defaultClusterName; // V1: Not needed. Moved to NFT. /// @notice Enumerate all names owned by a cluster - mapping(uint256 clusterId => EnumerableSetLib.Bytes32Set names) internal _clusterNames; + mapping(uint256 clusterId => EnumerableSetLib.Bytes32Set names) internal _clusterNames; // V1: Not needed. Moved to + // NFT. /// @notice For example lookup[17]["hot"] -> 0x123... - mapping(uint256 clusterId => mapping(bytes32 walletName => bytes32 addr)) public forwardLookup; + mapping(uint256 clusterId => mapping(bytes32 walletName => bytes32 addr)) public forwardLookup; // V1: Not needed. + // Moved offchain. /// @notice For example lookup[0x123...] -> "hot", then combine with cluster name in a diff method - mapping(bytes32 addr => bytes32 walletName) public reverseLookup; + mapping(bytes32 addr => bytes32 walletName) public reverseLookup; // V1: Not needed. Moved offchain. /// @notice Data required for proper harberger tax calculation when pokeName() is called - mapping(bytes32 name => IClustersHub.PriceIntegral integral) public priceIntegral; + mapping(bytes32 name => IClustersHub.PriceIntegral integral) public priceIntegral; // V1: Combine into a single + // BidData. /// @notice The amount of money backing each name registration - mapping(bytes32 name => uint256 amount) public nameBacking; + mapping(bytes32 name => uint256 amount) public nameBacking; // V1: Combine into a single BidData. /// @notice Bid info storage, all bidIds are incremental and are not sorted by name - mapping(bytes32 name => IClustersHub.Bid bidData) public bids; + mapping(bytes32 name => IClustersHub.Bid bidData) public bids; // V1: Combine into a single BidData. /// @notice Failed bid refunds are pooled so we don't have to revert when the highest bid is outbid - mapping(bytes32 bidder => uint256 refund) public bidRefunds; + mapping(bytes32 bidder => uint256 refund) public bidRefunds; // V1: Not needed. Just use force refund. /** * PROTOCOL INVARIANT TRACKING @@ -62,13 +65,13 @@ abstract contract NameManagerHub is IClustersHub { */ /// @notice Amount of eth that's transferred from nameBacking to the protocol - uint256 public protocolAccrual; + uint256 public protocolAccrual; // V1: Not needed. Use multicaller. /// @notice Amount of eth that's backing names - uint256 public totalNameBacking; + uint256 public totalNameBacking; // V1: Not needed. Use multicaller. /// @notice Amount of eth that's sitting in active bids and canceled but not-yet-withdrawn bids - uint256 public totalBidBacking; + uint256 public totalBidBacking; // V1: Not needed. Use multicaller. /// @dev Ensures balance invariant holds function _checkInvariant() internal view { diff --git a/test/ClustersMarketV1.t.sol b/test/ClustersMarketV1.t.sol new file mode 100644 index 0000000..0ed91c6 --- /dev/null +++ b/test/ClustersMarketV1.t.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol"; +import {LibSort} from "solady/utils/LibSort.sol"; +import {LibString} from "solady/utils/LibString.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {LibPRNG} from "solady/utils/LibPRNG.sol"; +import {PricingFlat} from "clusters/PricingFlat.sol"; +import "./utils/SoladyTest.sol"; +import "./mocks/MockClustersNFTV1.sol"; +import "./mocks/MockClustersMarketV1.sol"; + +contract ClustersMarketV1Test is SoladyTest { + using DynamicArrayLib for *; + + PricingFlat internal pricing; + MockClustersNFTV1 internal nft; + MockClustersMarketV1 internal market; + + address internal constant ALICE = address(0x11111); + address internal constant BOB = address(0x22222); + address internal constant CHARLIE = address(0x33333); + + function setUp() public { + pricing = PricingFlat(_smallAddress("pricing")); + vm.etch(address(pricing), LibClone.clone(address(new PricingFlat())).code); + nft = MockClustersNFTV1(_smallAddress("nft")); + vm.etch(address(nft), LibClone.clone(address(new MockClustersNFTV1())).code); + market = MockClustersMarketV1(LibClone.clone(address(new MockClustersMarketV1()))); + market.initialize(address(this)); + nft.initialize(address(this)); + market.setPricingContract(address(pricing)); + market.setNFTContract(address(nft)); + + nft.setRole(address(market), nft.MINTER_ROLE(), true); + nft.setRole(address(market), nft.CONDUIT_ROLE(), true); + } + + function testSetContractAddresses(address nftContract, address pricingContract) public { + if (uint160(nftContract) > type(uint128).max) { + nftContract = _smallAddress(abi.encode(nftContract)); + } + if (uint160(pricingContract) > type(uint128).max) { + pricingContract = _smallAddress(abi.encode(pricingContract)); + } + market.setNFTContract(nftContract); + market.setPricingContract(pricingContract); + assertEq(market.nftContract(), nftContract); + assertEq(market.pricingContract(), pricingContract); + } + + function testGetIntegratedPrice(uint256 lastUpdatedPrice, uint256 secondsSinceUpdate) public view { + lastUpdatedPrice = _bound(lastUpdatedPrice, 0, 0xffffffffffffffffffffffffffffffff); + secondsSinceUpdate = _bound(secondsSinceUpdate, 0, 0xffffffff); + (uint256 spent, uint256 price) = market.getIntegratedPrice(lastUpdatedPrice, secondsSinceUpdate); + (uint256 expectedSpent, uint256 expectedPrice) = + pricing.getIntegratedPrice(lastUpdatedPrice, secondsSinceUpdate); + assertEq(spent, expectedSpent); + assertEq(price, expectedPrice); + } + + function testMinAnnualPrice() public view { + assertEq(market.minAnnualPrice(), market.minAnnualPrice()); + } + + function testIsRegistered(bytes32) public { + bytes32 clusterName = _randomClusterName(); + uint256 minAnnualPrice = pricing.minAnnualPrice(); + + assertEq(market.isRegistered(clusterName), false); + + vm.deal(ALICE, 2 ** 160 - 1); + vm.prank(ALICE); + market.buy{value: minAnnualPrice}(clusterName); + + assertEq(market.isRegistered(clusterName), true); + + if (_randomChance(2)) { + market.directMove(clusterName, address(uint160(_bound(_randomUniform(), 1, 256)))); + assertEq(market.isRegistered(clusterName), false); + return; + } + if (_randomChance(2)) { + market.directMove(clusterName, address(uint160(_bound(_randomUniform(), 257, 0xffffffff)))); + assertEq(market.isRegistered(clusterName), true); + } + } + + function _randomClusterName() internal returns (bytes32 result) { + do { + uint256 m = 0x6161616161616161616161616161616161616161616161616161616161616161; + m |= _randomUniform() & 0x0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e; + m <<= (_randomUniform() & 31) << 3; + result = bytes32(m); + } while (LibString.normalizeSmallString(result) != result || result == bytes32(0)); + if (_randomChance(2)) result = bytes12(result); + } + + function _smallAddress(bytes memory seed) internal pure returns (address result) { + assembly ("memory-safe") { + result := and(keccak256(add(seed, 0x20), mload(seed)), 0xffffffffffffffffffffffffffffffff) + } + } + + struct _TestTemps { + uint256 minBidIncrement; + uint256 minAnnualPrice; + uint256 spent; + uint256 price; + bytes32 clusterName; + uint256 bidTimestamp; + uint256 bidAmount; + uint256 lastBacking; + uint256 bidIncrement; + uint256 bidDecrement; + uint256 lastBidderBalanceBefore; + uint256 lastBidAmount; + } + + function testBuyBidPoke(bytes32) public { + if (vm.getBlockTimestamp() == 0) { + vm.warp(100); + } + + vm.deal(ALICE, 2 ** 88 - 1); + vm.deal(BOB, 2 ** 88 - 1); + vm.deal(CHARLIE, 2 ** 88 - 1); + + vm.warp(_bound(_randomUniform(), 1, 256)); + + _TestTemps memory t; + t.minBidIncrement = market.minBidIncrement(); + t.clusterName = _randomClusterName(); + t.minAnnualPrice = pricing.minAnnualPrice(); + t.bidAmount = _bound(_random(), t.minAnnualPrice, 1 ether); + + vm.expectRevert(ClustersMarketV1.NameNotRegistered.selector); + market.bid{value: t.bidAmount}(t.clusterName); + + vm.expectRevert(ClustersMarketV1.Insufficient.selector); + vm.prank(ALICE); + market.buy(t.clusterName); + + vm.expectRevert(ClustersMarketV1.Insufficient.selector); + vm.prank(ALICE); + market.buy{value: t.minAnnualPrice - 1}(t.clusterName); + + // Test buy. + vm.prank(ALICE); + market.buy{value: t.minAnnualPrice}(t.clusterName); + + ClustersMarketV1.NameInfo memory info = market.nameInfo(t.clusterName); + assertEq(info.owner, ALICE); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + assertEq(info.backing, t.minAnnualPrice); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + + t.lastBacking = info.backing; + t.bidTimestamp = vm.getBlockTimestamp() + _bound(_random(), 0, 256); + vm.warp(t.bidTimestamp); + + (t.spent, t.price) = pricing.getIntegratedPrice(info.lastPrice, vm.getBlockTimestamp() - info.lastUpdated); + + // Test bid. + vm.prank(BOB); + market.bid{value: t.bidAmount}(t.clusterName); + + info = market.nameInfo(t.clusterName); + assertEq(info.owner, ALICE); + assertEq(info.bidAmount, t.bidAmount); + assertEq(info.bidUpdated, t.bidTimestamp); + assertEq(info.bidder, BOB); + assertEq(info.backing, t.lastBacking - t.spent); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + + // Test increase bid. + t.bidIncrement = _bound(_random(), t.minBidIncrement, t.minBidIncrement * 2); + vm.prank(BOB); + market.bid{value: t.bidIncrement}(t.clusterName); + + info = market.nameInfo(t.clusterName); + assertEq(info.owner, ALICE); + assertEq(info.bidAmount, t.bidAmount + t.bidIncrement); + assertEq(info.bidUpdated, t.bidTimestamp); + assertEq(info.bidder, BOB); + assertEq(info.backing, t.lastBacking - t.spent); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + + // Test outbid. + if (_randomChance(8)) { + t.lastBidderBalanceBefore = BOB.balance; + t.lastBidAmount = info.bidAmount; + + t.bidAmount = info.bidAmount + _bound(_random(), t.minBidIncrement, t.minBidIncrement * 2); + vm.prank(CHARLIE); + market.bid{value: t.bidAmount}(t.clusterName); + + info = market.nameInfo(t.clusterName); + assertEq(info.owner, ALICE); + assertEq(info.bidAmount, t.bidAmount); + assertEq(info.bidUpdated, vm.getBlockTimestamp()); + assertEq(info.bidder, CHARLIE); + assertEq(info.backing, t.lastBacking - t.spent); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + + assertEq(BOB.balance, t.lastBidderBalanceBefore + t.lastBidAmount); + return; + } + _checkInvariants(t); + + // Test poke after name has exhausted all backing. + if (_randomChance(8)) { + t.lastBidderBalanceBefore = BOB.balance; + t.lastBidAmount = info.bidAmount; + + // Test poke to send to winning bidder. + if (_randomChance(2)) { + vm.warp(vm.getBlockTimestamp() + 365 days); + market.poke(t.clusterName); + + info = market.nameInfo(t.clusterName); + assertEq(info.owner, BOB); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + assertEq(info.backing, t.lastBidAmount); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + return; + } + + // Test poke to send to stash if there's no bidder. + if (_randomChance(2)) { + vm.warp(vm.getBlockTimestamp() + market.bidTimelock()); + vm.prank(BOB); + market.reduceBid(t.clusterName, t.lastBidAmount); + + info = market.nameInfo(t.clusterName); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + assertEq(info.owner, ALICE); + + vm.warp(vm.getBlockTimestamp() + 365 days); + market.poke(t.clusterName); + + info = market.nameInfo(t.clusterName); + assertEq(info.owner, address(uint160((info.id & 0xff) + 1))); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + assertEq(info.backing, 0); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + return; + } + + // Test that a better bid will auto win the name, + if (_randomChance(2)) { + vm.warp(vm.getBlockTimestamp() + 365 days); + t.bidAmount = info.bidAmount + _bound(_random(), t.minBidIncrement, t.minBidIncrement * 2); + + vm.prank(CHARLIE); + market.bid{value: t.bidAmount}(t.clusterName); + + info = market.nameInfo(t.clusterName); + assertEq(info.owner, CHARLIE); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + assertEq(info.backing, t.bidAmount); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + + assertEq(BOB.balance, t.lastBidderBalanceBefore + t.lastBidAmount); + return; + } + + // Test reduce bid will poke and skip the entire reduce bid workflow. + vm.warp(vm.getBlockTimestamp() + 365 days); + vm.prank(BOB); + market.reduceBid(t.clusterName, t.lastBidAmount); + + info = market.nameInfo(t.clusterName); + assertEq(info.owner, BOB); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + assertEq(info.backing, t.lastBidAmount); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + + assertEq(BOB.balance, t.lastBidderBalanceBefore); + return; + } + _checkInvariants(t); + + // Test accept bid. + if (_randomChance(8)) { + t.lastBidAmount = info.bidAmount; + + vm.prank(ALICE); + market.acceptBid(t.clusterName); + info = market.nameInfo(t.clusterName); + assertEq(info.owner, BOB); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + assertEq(info.backing, t.lastBidAmount); + assertEq(info.lastUpdated, vm.getBlockTimestamp()); + assertEq(info.lastPrice, t.minAnnualPrice); + return; + } + _checkInvariants(t); + + // Test reduce bid. + if (_randomChance(8)) { + vm.warp(vm.getBlockTimestamp() + market.bidTimelock()); + uint256 maxDecrement = info.bidAmount - pricing.minAnnualPrice(); + + t.bidDecrement = _bound(_randomUniform(), 0, maxDecrement); + uint256 expectedBidAmount = info.bidAmount - t.bidDecrement; + if (_randomChance(2)) { + vm.prank(BOB); + vm.expectRevert(ClustersMarketV1.Insufficient.selector); + market.reduceBid(t.clusterName, maxDecrement + 1); + } + vm.prank(BOB); + market.reduceBid(t.clusterName, t.bidDecrement); + info = market.nameInfo(t.clusterName); + assertEq(info.bidAmount, expectedBidAmount); + assertEq(info.bidUpdated, vm.getBlockTimestamp()); + assertEq(info.bidder, BOB); + } + _checkInvariants(t); + + // Test refund bid. + if (_randomChance(8)) { + vm.warp(vm.getBlockTimestamp() + market.bidTimelock()); + uint256 delta = market.nameInfo(t.clusterName).bidAmount + _bound(_random(), 0, 256); + vm.prank(BOB); + market.reduceBid(t.clusterName, delta); + info = market.nameInfo(t.clusterName); + assertEq(info.bidAmount, 0); + assertEq(info.bidUpdated, 0); + assertEq(info.bidder, address(0)); + } + _checkInvariants(t); + } + + function _checkInvariants(_TestTemps memory t) internal view { + ClustersMarketV1.NameInfo memory info = market.nameInfo(t.clusterName); + uint256 totalBidBacking = market.totalBidBacking(); + assert(totalBidBacking >= info.bidAmount); + assert(address(market).balance >= totalBidBacking); + if (info.bidder != address(0)) { + assert(info.bidAmount != 0); + assert(info.bidUpdated != 0); + } else { + assert(info.bidAmount == 0); + assert(info.bidUpdated == 0); + } + } +} diff --git a/test/ClustersNFTV1.t.sol b/test/ClustersNFTV1.t.sol new file mode 100644 index 0000000..dcb3159 --- /dev/null +++ b/test/ClustersNFTV1.t.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol"; +import {LibSort} from "solady/utils/LibSort.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {LibPRNG} from "solady/utils/LibPRNG.sol"; +import {LibString} from "solady/utils/LibString.sol"; +import "./utils/SoladyTest.sol"; +import "./mocks/MockClustersNFTV1.sol"; +import "./mocks/MockClustersNFTBaseURIRenderer.sol"; + +contract ClustersNFTV1Test is SoladyTest { + using DynamicArrayLib for *; + + MockClustersNFTV1 internal nft; + MockClustersNFTBaseURIRenderer internal tokenURIRenderer; + + address internal constant ALICE = address(111); + address internal constant BOB = address(222); + + function setUp() public { + nft = MockClustersNFTV1(LibClone.clone(address(new MockClustersNFTV1()))); + nft.initialize(address(this)); + tokenURIRenderer = new MockClustersNFTBaseURIRenderer(); + tokenURIRenderer.setOwner(address(this)); + } + + function testSeedGas() public { + vm.pauseGasMetering(); + ClustersNFTV1.Mint[] memory mints = _randomSeeds(); + vm.resumeGasMetering(); + nft.mintNext(mints); + } + + function _hasDuplicates(bytes32[] memory a) internal pure returns (bool) { + bytes32[] memory aCopy = LibSort.copy(a); + LibSort.sort(aCopy); + LibSort.uniquifySorted(aCopy); + return aCopy.length != a.length; + } + + function _randomSeeds() internal returns (ClustersNFTV1.Mint[] memory mints) { + uint256 n = 100; + bytes32[] memory clusterNames = new bytes32[](n); + mints = new ClustersNFTV1.Mint[](n); + for (uint256 i; i < mints.length; ++i) { + ClustersNFTV1.Mint memory mint = mints[i]; + mint.clusterName = bytes12(_randomClusterName()); + mint.to = _randomNonZeroAddress(); + mint.initialTimestamp = _bound(_random(), 0, type(uint40).max); + mint.initialBacking = _bound(_random(), 0, type(uint88).max); + clusterNames[i] = mint.clusterName; + } + if (_hasDuplicates(clusterNames)) return _randomSeeds(); + } + + function testTokenURI(address to, uint256 id) public { + if (to == address(0)) to = _randomNonZeroAddress(); + nft.directMint(to, id); + nft.setTokenURIRenderer(address(tokenURIRenderer)); + tokenURIRenderer.setBaseURI("https://hehe.org/{id}.json"); + string memory expected = string(abi.encodePacked("https://hehe.org/", LibString.toString(id), ".json")); + assertEq(nft.tokenURI(id), expected); + } + + struct _TestTemps { + DynamicArrayLib.DynamicArray clusterNames; + DynamicArrayLib.DynamicArray recipients; + DynamicArrayLib.DynamicArray initialTimestamps; + DynamicArrayLib.DynamicArray initialBackings; + DynamicArrayLib.DynamicArray linkedAddresses; + } + + function testInitialDataAndLinkedAddress(bytes32) public { + ClustersNFTV1.Mint[] memory mints = new ClustersNFTV1.Mint[](1); + mints[0].clusterName = _randomClusterName(); + mints[0].to = _randomRecipient(); + mints[0].initialTimestamp = _bound(_random(), 0, type(uint40).max); + mints[0].initialBacking = _bound(_random(), 0, type(uint88).max); + assertEq(nft.mintNext(mints), 1); + (uint256 initialTimestamp, uint256 initialBacking) = nft.initialData(mints[0].clusterName); + assertEq(initialTimestamp, mints[0].initialTimestamp); + assertEq(initialBacking, mints[0].initialBacking); + assertEq(nft.linkedAddress(mints[0].clusterName), address(0)); + address linkedAddress = _randomAddress(); + vm.prank(mints[0].to); + nft.setLinkedAddress(mints[0].clusterName, linkedAddress); + (initialTimestamp, initialBacking) = nft.initialData(mints[0].clusterName); + assertEq(initialTimestamp, mints[0].initialTimestamp); + assertEq(initialBacking, mints[0].initialBacking); + assertEq(nft.linkedAddress(mints[0].clusterName), linkedAddress); + } + + function testMintNext(bytes32) public { + _TestTemps memory t = _randomTestTemps(); + + assertEq(nft.mintNext(_toMints(t)), 1); + + uint256 expected = nft.totalSupply() + 1; + + uint256[] memory newClusterNames; + do { + newClusterNames = _randomClusterNames(_randomNonZeroLength()).data; + newClusterNames = LibSort.difference(newClusterNames, t.clusterNames.data); + } while (newClusterNames.length == 0); + t.clusterNames = DynamicArrayLib.wrap(newClusterNames); + t.recipients = _randomRecipients(t.clusterNames.length()); + t.initialBackings = _randomInitialBackings(t.clusterNames.length()); + t.initialTimestamps = _randomInitialTimestamps(t.clusterNames.length()); + + assertEq(nft.mintNext(_toMints(t)), expected); + } + + function testMixed(bytes32) public { + _TestTemps memory t = _randomTestTemps(); + + assertEq(nft.defaultId(ALICE), 0); + assertEq(nft.defaultId(BOB), 0); + + nft.mintNext(_toMints(t)); + + if (_randomChance(8)) { + if (nft.balanceOf(ALICE) > 0) { + assertEq(nft.defaultId(ALICE), nft.tokensOfOwner(ALICE)[0]); + } + if (nft.balanceOf(BOB) > 0) { + assertEq(nft.defaultId(BOB), nft.tokensOfOwner(BOB)[0]); + } + } + + do { + if (_randomChance(2)) _testTransferForm(t); + if (_randomChance(2)) _checkInvariants(t); + if (_randomChance(8)) _testInitialDataAndSetAndGetLinkedAddresses(t); + if (_randomChance(2)) _testSetAndGetDefaultId(t); + } while (_randomChance(2)); + } + + function _testTransferForm(_TestTemps memory t) internal { + uint256 newTimestamp = vm.getBlockTimestamp() + (_random() % 100); + vm.warp(newTimestamp); + for (uint256 i; i < t.clusterNames.length(); ++i) { + if (_randomChance(2)) { + address recipient = _randomChance(2) ? ALICE : BOB; + uint256 id = i + 1; + address owner = nft.ownerOf(id); + vm.prank(owner); + nft.transferFrom(owner, recipient, id); + t.recipients.set(i, recipient); + + bytes32 clusterName = nft.nameOf(id); + assertEq(clusterName, t.clusterNames.getBytes32(i)); + (uint256 retrievedId,, uint256 startTimestamp) = nft.infoOf(clusterName); + assertEq(retrievedId, id); + if (owner != recipient) { + assertEq(startTimestamp, newTimestamp); + } + } + } + } + + function _testInitialDataAndSetAndGetLinkedAddresses(_TestTemps memory t) internal { + _checkInitialData(t); + for (uint256 i; i < t.clusterNames.length(); ++i) { + if (_randomChance(2)) { + address newLinkedAddress = _randomAddress(); + address owner = nft.ownerOf(i + 1); + vm.prank(owner); + nft.setLinkedAddress(t.clusterNames.getBytes32(i), newLinkedAddress); + t.linkedAddresses.set(i, newLinkedAddress); + } + } + _checkInitialData(t); + } + + function _testSetAndGetDefaultId(_TestTemps memory t) internal { + uint256 n = t.clusterNames.length(); + address owner = t.recipients.getAddress(_randomUniform() % n); + uint256 id = _bound(_randomUniform(), 0, n + 10); + vm.prank(owner); + nft.setDefaultId(id); + if (nft.nameOf(id) != "" && nft.ownerOf(id) == owner) { + assertEq(nft.defaultId(owner), id); + } else if (nft.balanceOf(owner) != 0) { + assertEq(nft.defaultId(owner), nft.tokensOfOwner(owner)[0]); + } + } + + function _checkInitialData(_TestTemps memory t) internal view { + for (uint256 i; i < t.clusterNames.length(); ++i) { + bytes32 clusterName = t.clusterNames.getBytes32(i); + (uint256 initialTimestamp, uint256 initialBacking) = nft.initialData(clusterName); + assertEq(initialTimestamp, t.initialTimestamps.get(i)); + assertEq(initialBacking, t.initialBackings.get(i)); + assertEq(nft.linkedAddress(clusterName), t.linkedAddresses.getAddress(i)); + } + } + + function _checkInvariants(_TestTemps memory t) internal { + unchecked { + if (_randomChance(2)) { + for (uint256 i; i != t.clusterNames.length(); ++i) { + assertEq(nft.ownerOf(i + 1), t.recipients.getAddress(i)); + assertEq(nft.nameOf(i + 1), t.clusterNames.getBytes32(i)); + } + } + uint256[] memory bobIds = nft.tokensOfOwner(BOB); + uint256[] memory aliceIds = nft.tokensOfOwner(ALICE); + if (_randomChance(2)) { + for (uint256 i; i != aliceIds.length; ++i) { + assertEq(nft.ownerOf(aliceIds.get(i)), ALICE); + assertEq(aliceIds.get(i), nft.tokenOfOwnerByIndex(ALICE, i)); + } + for (uint256 i; i != bobIds.length; ++i) { + assertEq(nft.ownerOf(bobIds.get(i)), BOB); + assertEq(bobIds.get(i), nft.tokenOfOwnerByIndex(BOB, i)); + } + } + if (_randomChance(2)) { + LibSort.sort(aliceIds); + LibSort.sort(bobIds); + uint256[] memory allIds = LibSort.union(aliceIds, bobIds); + assertEq(allIds.length, nft.totalSupply()); + for (uint256 i; i != allIds.length; ++i) { + uint256 id = allIds.get(i); + bytes32 clusterName = nft.nameOf(id); + (uint256 retrievedId,,) = nft.infoOf(clusterName); + assertEq(id, retrievedId); + } + } + } + } + + function _randomBigLength() internal returns (uint256) { + return _random() & 1023; + } + + function _randomBigNonZeroLength() internal returns (uint256 result) { + while (result == 0) result = _randomBigLength(); + } + + function _randomSmallLength() internal returns (uint256) { + return _random() & 15; + } + + function _randomSmallNonZeroLength() internal returns (uint256 result) { + while (result == 0) result = _randomSmallLength(); + } + + function _randomLength() internal returns (uint256) { + return _randomChance(128) ? _randomBigLength() : _randomSmallLength(); + } + + function _randomNonZeroLength() internal returns (uint256 result) { + while (result == 0) result = _randomLength(); + } + + function _randomTestTemps() internal returns (_TestTemps memory t) { + t.clusterNames = _randomClusterNames(_randomNonZeroLength()); + uint256 n = t.clusterNames.length(); + t.recipients = _randomRecipients(n); + t.initialTimestamps = _randomInitialTimestamps(n); + t.initialBackings = _randomInitialBackings(n); + t.linkedAddresses.resize(n); + } + + function _randomClusterNames(uint256 maxLength) internal returns (DynamicArrayLib.DynamicArray memory a) { + a.resize(maxLength); + unchecked { + for (uint256 i; i != maxLength; ++i) { + a.set(i, _randomClusterName()); + } + } + LibSort.sort(a.data); + LibSort.uniquifySorted(a.data); + } + + function _randomRecipients(uint256 n) internal returns (DynamicArrayLib.DynamicArray memory a) { + a.resize(n); + unchecked { + for (uint256 i; i != n; ++i) { + a.set(i, _randomRecipient()); + } + } + } + + function _randomInitialTimestamps(uint256 n) internal returns (DynamicArrayLib.DynamicArray memory a) { + a.resize(n); + unchecked { + for (uint256 i; i != n; ++i) { + a.set(i, _bound(_random(), 0, 2 ** 40 - 1)); + } + } + } + + function _randomInitialBackings(uint256 n) internal returns (DynamicArrayLib.DynamicArray memory a) { + a.resize(n); + unchecked { + for (uint256 i; i != n; ++i) { + a.set(i, _bound(_random(), 0, 2 ** 88 - 1)); + } + } + } + + function _toMints(_TestTemps memory t) internal pure returns (ClustersNFTV1.Mint[] memory) { + return _toMints(t.clusterNames, t.recipients, t.initialTimestamps, t.initialBackings); + } + + function _toMints( + DynamicArrayLib.DynamicArray memory names, + DynamicArrayLib.DynamicArray memory recipients, + DynamicArrayLib.DynamicArray memory initialTimestamps, + DynamicArrayLib.DynamicArray memory initialBackings + ) internal pure returns (ClustersNFTV1.Mint[] memory a) { + a = new ClustersNFTV1.Mint[](names.length()); + assertEq(names.length(), recipients.length()); + for (uint256 i; i < names.length(); ++i) { + ClustersNFTV1.Mint memory mint = a[i]; + mint.clusterName = names.getBytes32(i); + mint.to = recipients.getAddress(i); + mint.initialTimestamp = initialTimestamps.get(i); + mint.initialBacking = initialBackings.get(i); + } + } + + function testSetAndGetClustersData(bytes32) public { + do { + uint40 id0 = uint40(_random()); + uint40 id1 = uint40(_random()); + uint256 ownedIndex0 = _random(); + uint256 ownedIndex1 = _random(); + uint168 additionalData0 = uint168(_random()); + uint168 additionalData1 = uint168(_random()); + + nft.nameDataInitialize(0, id0, ownedIndex0); + nft.nameDataInitialize(1, id1, ownedIndex1); + assertEq(nft.nameDataGetId(0), id0); + assertEq(nft.nameDataGetId(1), id1); + assertEq(nft.nameDataGetOwnedIndex(0), ownedIndex0); + assertEq(nft.nameDataGetOwnedIndex(1), ownedIndex1); + + nft.nameDataSetAdditionalData(0, additionalData0); + nft.nameDataSetAdditionalData(1, additionalData1); + assertEq(nft.nameDataGetAdditionalData(0), additionalData0); + assertEq(nft.nameDataGetAdditionalData(1), additionalData1); + + ownedIndex0 = _random(); + ownedIndex1 = _random(); + additionalData0 = uint168(_random()); + additionalData1 = uint168(_random()); + + nft.nameDataSetOwnedIndex(0, ownedIndex0); + nft.nameDataSetOwnedIndex(1, ownedIndex1); + assertEq(nft.nameDataGetId(0), id0); + assertEq(nft.nameDataGetId(1), id1); + assertEq(nft.nameDataGetOwnedIndex(0), ownedIndex0); + assertEq(nft.nameDataGetOwnedIndex(1), ownedIndex1); + + nft.nameDataSetAdditionalData(0, additionalData0); + nft.nameDataSetAdditionalData(1, additionalData1); + assertEq(nft.nameDataGetAdditionalData(0), additionalData0); + assertEq(nft.nameDataGetAdditionalData(1), additionalData1); + } while (_randomChance(2)); + } + + function _randomClusterName() internal returns (bytes32 result) { + do { + uint256 m = 0x6161616161616161616161616161616161616161616161616161616161616161; + m |= _randomUniform() & 0x0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e; + m <<= (_randomUniform() & 31) << 3; + result = bytes32(m); + } while (LibString.normalizeSmallString(result) != result || result == bytes32(0)); + if (_randomChance(2)) result = bytes12(result); + } + + function _randomRecipient() internal returns (address) { + return _randomChance(2) ? ALICE : BOB; + } +} diff --git a/test/MessageHubV1.t.sol b/test/MessageHubV1.t.sol new file mode 100644 index 0000000..af61a42 --- /dev/null +++ b/test/MessageHubV1.t.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./utils/SoladyTest.sol"; +import "./mocks/MockMessageHubV1.sol"; +import "clusters/MessageInitiatorV1.sol"; +import "devtools/mocks/EndpointV2Mock.sol"; +import {LibClone} from "solady/utils/LibClone.sol"; +import {SafeCastLib} from "solady/utils/SafeCastLib.sol"; + +contract Target { + address public hub; + uint256 public x; + uint256 public msgValueDuringSetX; + address public msgSenderDuringSetX; + address public senderDuringSetX; + bytes32 public originalSenderDuringSetX; + uint256 public originalSenderTypeDuringSetX; + uint256 public refundAmount; + + function depositRefund() public payable { + refundAmount += msg.value; + } + + function setHub(address newHub) public { + hub = newHub; + } + + function setX(uint256 newX) public payable { + msgValueDuringSetX = msg.value; + msgSenderDuringSetX = msg.sender; + x = newX; + (bool success, bytes memory results) = hub.staticcall(""); + (senderDuringSetX, originalSenderDuringSetX, originalSenderTypeDuringSetX) = + abi.decode(results, (address, bytes32, uint256)); + require(success); + + (success,) = msg.sender.call{value: refundAmount}(""); + require(success); + refundAmount = 0; + } +} + +contract MessagehubV1Test is SoladyTest { + MockMessageHubV1 hub; + MessageInitiatorV1 initiator; + + EndpointV2Mock eid1; + EndpointV2Mock eid2; + + Target target; + + address constant ALICE = address(111); + + struct Call { + address target; + uint256 value; + bytes data; + } + + function setUp() public { + eid1 = new EndpointV2Mock(1); + eid2 = new EndpointV2Mock(2); + + hub = MockMessageHubV1(payable(LibClone.deployERC1967(address(new MockMessageHubV1())))); + hub.initialize(address(eid1), address(this)); + assertEq(hub.owner(), address(this)); + + initiator = MessageInitiatorV1(payable(LibClone.deployERC1967(address(new MessageInitiatorV1())))); + initiator.initialize(address(eid2), address(this)); + assertEq(initiator.owner(), address(this)); + + initiator.setPeer(1, bytes32(uint256(uint160(address(hub))))); + hub.setPeer(2, bytes32(uint256(uint160(address(initiator))))); + + initiator.setDstEid(1); + + eid1.setDestLzEndpoint(address(initiator), address(eid2)); + eid2.setDestLzEndpoint(address(hub), address(eid1)); + + target = new Target(); + target.setHub(address(hub)); + } + + function testSubAccountArgsDifferential(bytes32 originalSender, uint256 originalSenderType) public view { + bytes memory expected = + abi.encodePacked(keccak256(abi.encode(originalSender, originalSenderType)), address(hub)); + assertEq(hub.subAccountArgs(originalSender, originalSenderType), expected); + } + + function testCrosschain() public { + uint256 gas = 1000000; + uint256 value = 1 ether; + uint256 newX = _random(); + + bytes memory data = abi.encodeWithSignature("setX(uint256)", newX); + + uint256 nativeFee = initiator.quoteWithDefaultOptions(address(target), data, gas, value); + + vm.deal(ALICE, 10 ether); + + vm.prank(ALICE); + initiator.sendWithDefaultOptions{value: nativeFee}(address(target), data, gas, value); + + assertEq(target.x(), newX); + assertEq(target.msgValueDuringSetX(), 1 ether); + assertEq(target.msgSenderDuringSetX(), address(hub)); + + assertEq(target.originalSenderDuringSetX(), bytes32(uint256(uint160(ALICE)))); + assertEq(target.originalSenderTypeDuringSetX(), 0); + assertEq(target.senderDuringSetX(), ALICE); + } + + struct _TestMothershipTemps { + bool originalSenderIsEthereumAddress; + bytes32 originalSender; + uint256 originalSenderType; + address sender; + bytes message; + uint256 newX; + uint256 refundAmount; + } + + function testMothership(bytes32) public { + _TestMothershipTemps memory t; + + t.originalSenderIsEthereumAddress = _randomChance(2); + + if (t.originalSenderIsEthereumAddress) { + t.sender = _randomNonZeroAddress(); + t.originalSenderType = 0; + t.originalSender = bytes32(uint256(uint160(t.sender))); + } else { + t.originalSender = bytes32(_random()); + t.originalSenderType = 1 | _random(); + while (t.originalSender == bytes32(0)) t.originalSender = bytes32(_random()); + t.sender = hub.predictSubAccount(t.originalSender, t.originalSenderType); + assertEq(hub.createSubAccount(t.originalSender, t.originalSenderType), t.sender); + } + t.newX = _random(); + + vm.deal(address(this), 10 ether); + vm.deal(address(hub), _random() % 0.1 ether); + + t.message = abi.encode( + t.originalSender, t.originalSenderType, address(target), abi.encodeWithSignature("setX(uint256)", t.newX) + ); + + vm.deal(address(this), 10 ether); + t.refundAmount = _random() % 1 ether; + target.depositRefund{value: t.refundAmount}(); + + hub.forward{value: 1 ether}(t.message); + + assertEq(target.msgValueDuringSetX(), 1 ether); + assertEq(target.msgSenderDuringSetX(), address(hub)); + assertEq(target.originalSenderDuringSetX(), t.originalSender); + assertEq(target.originalSenderTypeDuringSetX(), t.originalSenderType); + assertEq(target.senderDuringSetX(), t.sender); + + assertEq(target.x(), t.newX); + assertEq(t.sender.balance, t.refundAmount); + } +} diff --git a/test/mocks/MockClustersMarketV1.sol b/test/mocks/MockClustersMarketV1.sol new file mode 100644 index 0000000..d434823 --- /dev/null +++ b/test/mocks/MockClustersMarketV1.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "clusters/ClustersMarketV1.sol"; + +contract MockClustersMarketV1 is ClustersMarketV1 { + function getIntegratedPrice(uint256 lastUpdatedPrice, uint256 secondsSinceUpdate) + public + view + returns (uint256 spent, uint256 price) + { + return _getIntegratedPrice(_getClustersMarketStorage().contracts, lastUpdatedPrice, secondsSinceUpdate); + } + + function minAnnualPrice() public view returns (uint256) { + return _minAnnualPrice(_getClustersMarketStorage().contracts); + } + + function directMove(bytes32 clusterName, address to) public { + uint256 contracts = _getClustersMarketStorage().contracts; + uint256 packedInfo = _packedInfo(contracts, clusterName); + _move(contracts, to, packedInfo); + } +} diff --git a/test/mocks/MockClustersNFTBaseURIRenderer.sol b/test/mocks/MockClustersNFTBaseURIRenderer.sol new file mode 100644 index 0000000..407782c --- /dev/null +++ b/test/mocks/MockClustersNFTBaseURIRenderer.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "clusters/ClustersNFTBaseURIRenderer.sol"; + +contract MockClustersNFTBaseURIRenderer is ClustersNFTBaseURIRenderer { + function setOwner(address newOwner) public { + _setOwner(newOwner); + } +} diff --git a/test/mocks/MockClustersNFTV1.sol b/test/mocks/MockClustersNFTV1.sol new file mode 100644 index 0000000..8f28b0b --- /dev/null +++ b/test/mocks/MockClustersNFTV1.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "clusters/ClustersNFTV1.sol"; + +contract MockClustersNFTV1 is ClustersNFTV1 { + using DynamicArrayLib for *; + + NameData[2] internal _data; + + function directMint(address to, uint256 id) public { + _mintAndSetExtraDataUnchecked(to, id, 0); + } + + /// @dev Initializes the data. + function _initialize(NameData storage data, uint256 id, uint256 ownedIndex) internal { + if (ownedIndex <= 254) { + data.packed = _setByte(id | (block.timestamp << 48), 26, 0xff ^ ownedIndex); + } else { + data.packed = id | (block.timestamp << 48); + data.fullOwnedIndex = ownedIndex; + } + } + + /// @dev Returns the ID. + function _getId(NameData storage data) internal view returns (uint40) { + return uint40(data.packed); + } + + /// @dev Returns the owned index. + function _getOwnedIndex(NameData storage data) internal view returns (uint256) { + uint256 ownedIndex = uint8(bytes32(data.packed)[26]); + return ownedIndex != 0 ? 0xff ^ ownedIndex : data.fullOwnedIndex; + } + + /// @dev Sets the owned index. + function _setOwnedIndex(NameData storage data, uint256 ownedIndex) internal { + if (ownedIndex <= 254) { + data.packed = _setByte(data.packed, 26, 0xff ^ ownedIndex); + } else { + data.packed = _setByte(data.packed, 26, 0); + data.fullOwnedIndex = ownedIndex; + } + } + + /// @dev Updates the start timestamp. + function _updateStartTimestamp(NameData storage data) internal { + uint256 p = data.packed; + data.packed = p ^ (((p >> 48) ^ block.timestamp) & 0xffffffffff) << 48; + } + + /// @dev Returns the start timestamp. + function _getStartTimestamp(NameData storage data) internal view returns (uint256) { + return (data.packed >> 48) & 0xffffffffff; + } + + /// @dev Sets the additional data. + function _setAdditionalData(NameData storage data, uint168 additionalData) internal { + assembly ("memory-safe") { + mstore(0x0b, sload(data.slot)) + mstore(0x00, additionalData) + sstore(data.slot, mload(0x0b)) + } + } + + /// @dev Returns the additional data. + function _getAdditionalData(NameData storage data) internal view returns (uint168) { + return uint168(data.packed >> 88); + } + + function nameDataInitialize(uint256 i, uint40 id, uint256 ownedIndex) public { + _initialize(_data[i], id, ownedIndex); + } + + function nameDataGetId(uint256 i) public view returns (uint256) { + return _getId(_data[i]); + } + + function nameDataGetOwnedIndex(uint256 i) public view returns (uint256) { + return _getOwnedIndex(_data[i]); + } + + function nameDataSetOwnedIndex(uint256 i, uint256 ownedIndex) public { + return _setOwnedIndex(_data[i], ownedIndex); + } + + function nameDataSetAdditionalData(uint256 i, uint168 additionalData) public { + return _setAdditionalData(_data[i], additionalData); + } + + function nameDataGetAdditionalData(uint256 i) public view returns (uint168) { + return _getAdditionalData(_data[i]); + } +} diff --git a/test/mocks/MockMessageHubV1.sol b/test/mocks/MockMessageHubV1.sol new file mode 100644 index 0000000..765ce28 --- /dev/null +++ b/test/mocks/MockMessageHubV1.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "clusters/MessageHubV1.sol"; + +contract MockMessageHubV1 is MessageHubV1 { + function createSubAccount(bytes32 originalSender, uint256 senderType) public returns (address) { + return _createSubAccount(originalSender, senderType); + } + + function forward(bytes calldata message) public payable forwardMessage(message) {} + + function subAccountArgs(bytes32 originalSender, uint256 originalSenderType) public view returns (bytes memory) { + return _subAccountArgs(originalSender, originalSenderType); + } +} diff --git a/test/utils/Brutalizer.sol b/test/utils/Brutalizer.sol new file mode 100644 index 0000000..8bc8165 --- /dev/null +++ b/test/utils/Brutalizer.sol @@ -0,0 +1,885 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @dev WARNING! This mock is strictly intended for testing purposes only. +/// Do NOT copy anything here into production code unless you really know what you are doing. +contract Brutalizer { + /// @dev Multiplier for a mulmod Lehmer psuedorandom number generator. + /// Prime, and a primitive root of `_LPRNG_MODULO`. + uint256 private constant _LPRNG_MULTIPLIER = 0x100000000000000000000000000000051; + + /// @dev Modulo for a mulmod Lehmer psuedorandom number generator. (prime) + uint256 private constant _LPRNG_MODULO = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43; + + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + function _brutalizeMemory() internal view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(add(offset, 0x20), zero, calldatasize()) + mstore(offset, add(caller(), gas())) + + // Fill the 64 bytes of scratch space with garbage. + let r := keccak256(offset, add(calldatasize(), 0x40)) + mstore(zero, r) + mstore(0x20, keccak256(zero, 0x40)) + r := mulmod(mload(0x10), _LPRNG_MULTIPLIER, _LPRNG_MODULO) + + let cSize := add(codesize(), iszero(codesize())) + if iszero(lt(cSize, 32)) { cSize := sub(cSize, and(mload(0x02), 0x1f)) } + let start := mod(mload(0x10), cSize) + let size := mul(sub(cSize, start), gt(cSize, start)) + let times := div(0x7ffff, cSize) + if iszero(lt(times, 128)) { times := 128 } + + // Occasionally offset the offset by a pseudorandom large amount. + // Can't be too large, or we will easily get out-of-gas errors. + offset := add(offset, mul(iszero(and(r, 0xf00000000)), and(shr(64, r), 0xfffff))) + + // Fill the free memory with garbage. + // prettier-ignore + for { let w := not(0) } 1 {} { + mstore(offset, mload(0x00)) + mstore(add(offset, 0x20), mload(0x20)) + offset := add(offset, 0x40) + // We use codecopy instead of the identity precompile + // to avoid polluting the `forge test -vvvv` output with tons of junk. + codecopy(offset, start, size) + codecopy(add(offset, size), 0x00, start) + offset := add(offset, cSize) + times := add(times, w) // `sub(times, 1)`. + if iszero(times) { break } + } + // With a 1/16 chance, copy the contract's code to the scratch space. + if iszero(and(0xf00, r)) { + codecopy(0x00, mod(shr(128, r), add(codesize(), codesize())), 0x40) + mstore8(and(r, 0x3f), iszero(and(0x100000, r))) + } + } + } + + /// @dev Fills the scratch space with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + function _brutalizeScratchSpace() internal view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(add(offset, 0x20), zero, calldatasize()) + mstore(offset, add(caller(), gas())) + + // Fill the 64 bytes of scratch space with garbage. + let r := keccak256(offset, add(calldatasize(), 0x40)) + mstore(zero, r) + mstore(0x20, keccak256(zero, 0x40)) + r := mulmod(mload(0x10), _LPRNG_MULTIPLIER, _LPRNG_MODULO) + if iszero(and(0xf00, r)) { + codecopy(0x00, mod(shr(128, r), add(codesize(), codesize())), 0x40) + mstore8(and(r, 0x3f), iszero(and(0x100000, r))) + } + } + } + + /// @dev Fills the lower memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + /// For efficiency, this only fills a small portion of the free memory. + function _brutalizeLowerMemory() internal view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(add(offset, 0x20), zero, calldatasize()) + mstore(offset, add(caller(), gas())) + + // Fill the 64 bytes of scratch space with garbage. + let r := keccak256(offset, add(calldatasize(), 0x40)) + mstore(zero, r) + mstore(0x20, keccak256(zero, 0x40)) + r := mulmod(mload(0x10), _LPRNG_MULTIPLIER, _LPRNG_MODULO) + + for {} 1 {} { + if iszero(and(0x7000, r)) { + let x := keccak256(zero, 0x40) + mstore(offset, x) + mstore(add(0x20, offset), x) + mstore(add(0x40, offset), x) + mstore(add(0x60, offset), x) + mstore(add(0x80, offset), x) + mstore(add(0xa0, offset), x) + mstore(add(0xc0, offset), x) + mstore(add(0xe0, offset), x) + mstore(add(0x100, offset), x) + mstore(add(0x120, offset), x) + mstore(add(0x140, offset), x) + mstore(add(0x160, offset), x) + mstore(add(0x180, offset), x) + mstore(add(0x1a0, offset), x) + mstore(add(0x1c0, offset), x) + mstore(add(0x1e0, offset), x) + mstore(add(0x200, offset), x) + mstore(add(0x220, offset), x) + mstore(add(0x240, offset), x) + mstore(add(0x260, offset), x) + break + } + codecopy(offset, byte(0, r), codesize()) + break + } + if iszero(and(0x300, r)) { + codecopy(0x00, mod(shr(128, r), add(codesize(), codesize())), 0x40) + mstore8(and(r, 0x3f), iszero(and(0x100000, r))) + } + } + } + + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeMemory() { + _brutalizeMemory(); + _; + _checkMemory(); + } + + /// @dev Fills the scratch space with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeScratchSpace() { + _brutalizeScratchSpace(); + _; + _checkMemory(); + } + + /// @dev Fills the lower memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeLowerMemory() { + _brutalizeLowerMemory(); + _; + _checkMemory(); + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalized(address value) internal pure returns (address result) { + uint256 r = uint256(uint160(value)); + r = (__brutalizerRandomness(r) << 160) ^ r; + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint8(uint8 value) internal pure returns (uint8 result) { + uint256 r = (__brutalizerRandomness(value) << 8) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes1(bytes1 value) internal pure returns (bytes1 result) { + bytes32 r = __brutalizedBytesN(value, 8); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint16(uint16 value) internal pure returns (uint16 result) { + uint256 r = (__brutalizerRandomness(value) << 16) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes2(bytes2 value) internal pure returns (bytes2 result) { + bytes32 r = __brutalizedBytesN(value, 16); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint24(uint24 value) internal pure returns (uint24 result) { + uint256 r = (__brutalizerRandomness(value) << 24) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes3(bytes3 value) internal pure returns (bytes3 result) { + bytes32 r = __brutalizedBytesN(value, 24); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint32(uint32 value) internal pure returns (uint32 result) { + uint256 r = (__brutalizerRandomness(value) << 32) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes4(bytes4 value) internal pure returns (bytes4 result) { + bytes32 r = __brutalizedBytesN(value, 32); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint40(uint40 value) internal pure returns (uint40 result) { + uint256 r = (__brutalizerRandomness(value) << 40) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes5(bytes5 value) internal pure returns (bytes5 result) { + bytes32 r = __brutalizedBytesN(value, 40); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint48(uint48 value) internal pure returns (uint48 result) { + uint256 r = (__brutalizerRandomness(value) << 48) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes6(bytes6 value) internal pure returns (bytes6 result) { + bytes32 r = __brutalizedBytesN(value, 48); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint56(uint56 value) internal pure returns (uint56 result) { + uint256 r = (__brutalizerRandomness(value) << 56) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes7(bytes7 value) internal pure returns (bytes7 result) { + bytes32 r = __brutalizedBytesN(value, 56); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint64(uint64 value) internal pure returns (uint64 result) { + uint256 r = (__brutalizerRandomness(value) << 64) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes8(bytes8 value) internal pure returns (bytes8 result) { + bytes32 r = __brutalizedBytesN(value, 64); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint72(uint72 value) internal pure returns (uint72 result) { + uint256 r = (__brutalizerRandomness(value) << 72) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes9(bytes9 value) internal pure returns (bytes9 result) { + bytes32 r = __brutalizedBytesN(value, 72); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint80(uint80 value) internal pure returns (uint80 result) { + uint256 r = (__brutalizerRandomness(value) << 80) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes10(bytes10 value) internal pure returns (bytes10 result) { + bytes32 r = __brutalizedBytesN(value, 80); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint88(uint88 value) internal pure returns (uint88 result) { + uint256 r = (__brutalizerRandomness(value) << 88) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes11(bytes11 value) internal pure returns (bytes11 result) { + bytes32 r = __brutalizedBytesN(value, 88); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint96(uint96 value) internal pure returns (uint96 result) { + uint256 r = (__brutalizerRandomness(value) << 96) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes12(bytes12 value) internal pure returns (bytes12 result) { + bytes32 r = __brutalizedBytesN(value, 96); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint104(uint104 value) internal pure returns (uint104 result) { + uint256 r = (__brutalizerRandomness(value) << 104) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes13(bytes13 value) internal pure returns (bytes13 result) { + bytes32 r = __brutalizedBytesN(value, 104); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint112(uint112 value) internal pure returns (uint112 result) { + uint256 r = (__brutalizerRandomness(value) << 112) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes14(bytes14 value) internal pure returns (bytes14 result) { + bytes32 r = __brutalizedBytesN(value, 112); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint120(uint120 value) internal pure returns (uint120 result) { + uint256 r = (__brutalizerRandomness(value) << 120) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes15(bytes15 value) internal pure returns (bytes15 result) { + bytes32 r = __brutalizedBytesN(value, 120); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint128(uint128 value) internal pure returns (uint128 result) { + uint256 r = (__brutalizerRandomness(value) << 128) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes16(bytes16 value) internal pure returns (bytes16 result) { + bytes32 r = __brutalizedBytesN(value, 128); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint136(uint136 value) internal pure returns (uint136 result) { + uint256 r = (__brutalizerRandomness(value) << 136) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes17(bytes17 value) internal pure returns (bytes17 result) { + bytes32 r = __brutalizedBytesN(value, 136); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint144(uint144 value) internal pure returns (uint144 result) { + uint256 r = (__brutalizerRandomness(value) << 144) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes18(bytes18 value) internal pure returns (bytes18 result) { + bytes32 r = __brutalizedBytesN(value, 144); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint152(uint152 value) internal pure returns (uint152 result) { + uint256 r = (__brutalizerRandomness(value) << 152) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes19(bytes19 value) internal pure returns (bytes19 result) { + bytes32 r = __brutalizedBytesN(value, 152); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint160(uint160 value) internal pure returns (uint160 result) { + uint256 r = (__brutalizerRandomness(value) << 160) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes20(bytes20 value) internal pure returns (bytes20 result) { + bytes32 r = __brutalizedBytesN(value, 160); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint168(uint168 value) internal pure returns (uint168 result) { + uint256 r = (__brutalizerRandomness(value) << 168) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes21(bytes21 value) internal pure returns (bytes21 result) { + bytes32 r = __brutalizedBytesN(value, 168); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint176(uint176 value) internal pure returns (uint176 result) { + uint256 r = (__brutalizerRandomness(value) << 176) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes22(bytes22 value) internal pure returns (bytes22 result) { + bytes32 r = __brutalizedBytesN(value, 176); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint184(uint184 value) internal pure returns (uint184 result) { + uint256 r = (__brutalizerRandomness(value) << 184) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes23(bytes23 value) internal pure returns (bytes23 result) { + bytes32 r = __brutalizedBytesN(value, 184); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint192(uint192 value) internal pure returns (uint192 result) { + uint256 r = (__brutalizerRandomness(value) << 192) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes24(bytes24 value) internal pure returns (bytes24 result) { + bytes32 r = __brutalizedBytesN(value, 192); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint200(uint200 value) internal pure returns (uint200 result) { + uint256 r = (__brutalizerRandomness(value) << 200) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes25(bytes25 value) internal pure returns (bytes25 result) { + bytes32 r = __brutalizedBytesN(value, 200); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint208(uint208 value) internal pure returns (uint208 result) { + uint256 r = (__brutalizerRandomness(value) << 208) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes26(bytes26 value) internal pure returns (bytes26 result) { + bytes32 r = __brutalizedBytesN(value, 208); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint216(uint216 value) internal pure returns (uint216 result) { + uint256 r = (__brutalizerRandomness(value) << 216) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes27(bytes27 value) internal pure returns (bytes27 result) { + bytes32 r = __brutalizedBytesN(value, 216); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint224(uint224 value) internal pure returns (uint224 result) { + uint256 r = (__brutalizerRandomness(value) << 224) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes28(bytes28 value) internal pure returns (bytes28 result) { + bytes32 r = __brutalizedBytesN(value, 224); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint232(uint232 value) internal pure returns (uint232 result) { + uint256 r = (__brutalizerRandomness(value) << 232) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes29(bytes29 value) internal pure returns (bytes29 result) { + bytes32 r = __brutalizedBytesN(value, 232); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint240(uint240 value) internal pure returns (uint240 result) { + uint256 r = (__brutalizerRandomness(value) << 240) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes30(bytes30 value) internal pure returns (bytes30 result) { + bytes32 r = __brutalizedBytesN(value, 240); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalizedUint248(uint248 value) internal pure returns (uint248 result) { + uint256 r = (__brutalizerRandomness(value) << 248) ^ uint256(value); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the lower bits dirtied. + function _brutalizedBytes31(bytes31 value) internal pure returns (bytes31 result) { + bytes32 r = __brutalizedBytesN(value, 248); + /// @solidity memory-safe-assembly + assembly { + result := r + } + } + + /// @dev Returns the result with the upper bits dirtied. + function _brutalized(bool value) internal pure returns (bool result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + calldatacopy(result, 0x00, calldatasize()) + mstore(0x20, keccak256(result, calldatasize())) + mstore(0x10, xor(value, mload(0x10))) + let r := keccak256(0x00, 0x88) + mstore(0x10, r) + result := mul(iszero(iszero(value)), r) + if iszero(and(1, shr(128, mulmod(r, _LPRNG_MULTIPLIER, _LPRNG_MODULO)))) { + result := iszero(iszero(result)) + } + } + } + + /// @dev Returns a brutalizer randomness. + function __brutalizedBytesN(bytes32 x, uint256 s) private pure returns (bytes32) { + return bytes32(uint256((__brutalizerRandomness(uint256(x)) >> s) ^ uint256(x))); + } + + /// @dev Returns a brutalizer randomness. + function __brutalizerRandomness(uint256 seed) private pure returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + calldatacopy(result, 0x00, calldatasize()) + mstore(0x20, keccak256(result, calldatasize())) + mstore(0x10, xor(seed, mload(0x10))) + result := keccak256(0x00, 0x88) + mstore(0x10, result) + if iszero(and(7, shr(128, mulmod(result, _LPRNG_MULTIPLIER, _LPRNG_MODULO)))) { result := 0 } + } + } + + /// @dev Misaligns the free memory pointer. + /// The free memory pointer has a 1/32 chance to be aligned. + function _misalignFreeMemoryPointer() internal pure { + uint256 twoWords = 0x40; + /// @solidity memory-safe-assembly + assembly { + let m := mload(twoWords) + m := add(m, mul(and(keccak256(0x00, twoWords), 0x1f), iszero(and(m, 0x1f)))) + mstore(twoWords, m) + } + } + + /// @dev Check if the free memory pointer and the zero slot are not contaminated. + /// Useful for cases where these slots are used for temporary storage. + function _checkMemory() internal pure { + bool zeroSlotIsNotZero; + bool freeMemoryPointerOverflowed; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + // Test at a lower, but reasonable limit for more safety room. + if gt(mload(0x40), 0xffffffff) { freeMemoryPointerOverflowed := 1 } + // Check the value of the zero slot. + zeroSlotIsNotZero := mload(0x60) + } + if (freeMemoryPointerOverflowed) revert("`0x40` overflowed!"); + if (zeroSlotIsNotZero) revert("`0x60` is not zero!"); + } + + /// @dev Check if `s`: + /// - Has sufficient memory allocated. + /// - Is zero right padded (cuz some frontends like Etherscan has issues + /// with decoding non-zero-right-padded strings). + function _checkMemory(bytes memory s) internal pure { + bool notZeroRightPadded; + bool insufficientMalloc; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + let length := mload(s) + let lastWord := mload(add(add(s, 0x20), and(length, not(0x1f)))) + let remainder := and(length, 0x1f) + if remainder { if shl(mul(8, remainder), lastWord) { notZeroRightPadded := 1 } } + // Check if the memory allocated is sufficient. + if length { if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } } + } + if (notZeroRightPadded) revert("Not zero right padded!"); + if (insufficientMalloc) revert("Insufficient memory allocation!"); + _checkMemory(); + } + + /// @dev For checking the memory allocation for string `s`. + function _checkMemory(string memory s) internal pure { + _checkMemory(bytes(s)); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(uint256[] memory a) internal pure { + bool insufficientMalloc; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + // Check if the memory allocated is sufficient. + insufficientMalloc := gt(add(add(a, 0x20), shl(5, mload(a))), mload(0x40)) + } + if (insufficientMalloc) revert("Insufficient memory allocation!"); + _checkMemory(); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(bytes32[] memory a) internal pure { + uint256[] memory casted; + /// @solidity memory-safe-assembly + assembly { + casted := a + } + _checkMemory(casted); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(address[] memory a) internal pure { + uint256[] memory casted; + /// @solidity memory-safe-assembly + assembly { + casted := a + } + _checkMemory(casted); + } + + /// @dev Check if `a`: + /// - Has sufficient memory allocated. + function _checkMemory(bool[] memory a) internal pure { + uint256[] memory casted; + /// @solidity memory-safe-assembly + assembly { + casted := a + } + _checkMemory(casted); + } +} diff --git a/test/utils/SoladyTest.sol b/test/utils/SoladyTest.sol new file mode 100644 index 0000000..21c29f6 --- /dev/null +++ b/test/utils/SoladyTest.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "forge-std/Test.sol"; +import "./TestPlus.sol"; + +contract SoladyTest is Test, TestPlus { + /// @dev Alias for `_hem`. + function _bound(uint256 x, uint256 min, uint256 max) internal pure virtual override returns (uint256) { + return _hem(x, min, max); + } +} diff --git a/test/utils/TestPlus.sol b/test/utils/TestPlus.sol new file mode 100644 index 0000000..52ec513 --- /dev/null +++ b/test/utils/TestPlus.sol @@ -0,0 +1,686 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Brutalizer} from "./Brutalizer.sol"; + +contract TestPlus is Brutalizer { + event LogString(string name, string value); + event LogString(string value); + event LogBytes(string name, bytes value); + event LogBytes(bytes value); + event LogUint(string name, uint256 value); + event LogUint(uint256 value); + event LogBytes32(string name, bytes32 value); + event LogBytes32(bytes32 value); + event LogInt(string name, int256 value); + event LogInt(int256 value); + event LogAddress(string name, address value); + event LogAddress(address value); + event LogBool(string name, bool value); + event LogBool(bool value); + + event LogStringArray(string name, string[] value); + event LogStringArray(string[] value); + event LogBytesArray(string name, bytes[] value); + event LogBytesArray(bytes[] value); + event LogUintArray(string name, uint256[] value); + event LogUintArray(uint256[] value); + event LogBytes32Array(string name, bytes32[] value); + event LogBytes32Array(bytes32[] value); + event LogIntArray(string name, int256[] value); + event LogIntArray(int256[] value); + event LogAddressArray(string name, address[] value); + event LogAddressArray(address[] value); + event LogBoolArray(string name, bool[] value); + event LogBoolArray(bool[] value); + + /// @dev `address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))`. + address private constant _VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + + /// @dev This is the keccak256 of a very long string I randomly mashed on my keyboard. + uint256 private constant _TESTPLUS_RANDOMNESS_SLOT = + 0xd715531fe383f818c5f158c342925dcf01b954d24678ada4d07c36af0f20e1ee; + + /// @dev The maximum private key. + uint256 private constant _PRIVATE_KEY_MAX = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140; + + /// @dev Some constant to brutalize the upper bits of addresses. + uint256 private constant _ADDRESS_BRUTALIZER = 0xc0618c2bfd481dcf3e31738f; + + /// @dev Multiplier for a mulmod Lehmer psuedorandom number generator. + /// Prime, and a primitive root of `_LPRNG_MODULO`. + uint256 private constant _LPRNG_MULTIPLIER = 0x100000000000000000000000000000051; + + /// @dev Modulo for a mulmod Lehmer psuedorandom number generator. (prime) + uint256 private constant _LPRNG_MODULO = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43; + + /// @dev Returns whether the `value` has been generated for `typeId` and `groupId` before. + function __markAsGenerated(bytes32 typeId, bytes32 groupId, uint256 value) private returns (bool isSet) { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Cache the free memory pointer. + mstore(0x00, value) + mstore(0x20, groupId) + mstore(0x40, typeId) + mstore(0x60, _TESTPLUS_RANDOMNESS_SLOT) + let s := keccak256(0x00, 0x80) + isSet := sload(s) + sstore(s, 1) + mstore(0x40, m) // Restore the free memory pointer. + mstore(0x60, 0) // Restore the zero pointer. + } + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + /// This function may return a previously returned result. + function _random() internal returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := _TESTPLUS_RANDOMNESS_SLOT + let sValue := sload(result) + mstore(0x20, sValue) + let r := keccak256(0x20, 0x40) + // If the storage is uninitialized, initialize it to the keccak256 of the calldata. + if iszero(sValue) { + sValue := result + calldatacopy(mload(0x40), 0x00, calldatasize()) + r := keccak256(mload(0x40), calldatasize()) + } + sstore(result, add(r, 1)) + + // Do some biased sampling for more robust tests. + // prettier-ignore + for {} 1 {} { + let y := mulmod(r, _LPRNG_MULTIPLIER, _LPRNG_MODULO) + // With a 1/256 chance, randomly set `r` to any of 0,1,2,3. + if iszero(byte(19, y)) { + r := and(byte(11, y), 3) + break + } + let d := byte(17, y) + // With a 1/2 chance, set `r` to near a random power of 2. + if iszero(and(2, d)) { + // Set `t` either `not(0)` or `xor(sValue, r)`. + let t := or(xor(sValue, r), sub(0, and(1, d))) + // Set `r` to `t` shifted left or right. + // prettier-ignore + for {} 1 {} { + if iszero(and(8, d)) { + if iszero(and(16, d)) { t := 1 } + if iszero(and(32, d)) { + r := add(shl(shl(3, and(byte(7, y), 31)), t), sub(3, and(7, r))) + break + } + r := add(shl(byte(7, y), t), sub(511, and(1023, r))) + break + } + if iszero(and(16, d)) { t := shl(255, 1) } + if iszero(and(32, d)) { + r := add(shr(shl(3, and(byte(7, y), 31)), t), sub(3, and(7, r))) + break + } + r := add(shr(byte(7, y), t), sub(511, and(1023, r))) + break + } + // With a 1/2 chance, negate `r`. + r := xor(sub(0, shr(7, d)), r) + break + } + // Otherwise, just set `r` to `xor(sValue, r)`. + r := xor(sValue, r) + break + } + result := r + } + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function _randomUnique(uint256 groupId) internal returns (uint256 result) { + result = _randomUnique(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function _randomUnique(bytes32 groupId) internal returns (uint256 result) { + do { + result = _random(); + } while (__markAsGenerated("uint256", groupId, result)); + } + + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function _randomUnique() internal returns (uint256 result) { + result = _randomUnique(""); + } + + /// @dev Returns a pseudorandom number, uniformly distributed in [0 .. 2**256 - 1] (inclusive). + function _randomUniform() internal returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := _TESTPLUS_RANDOMNESS_SLOT + // prettier-ignore + for { let sValue := sload(result) } 1 {} { + // If the storage is uninitialized, initialize it to the keccak256 of the calldata. + if iszero(sValue) { + calldatacopy(mload(0x40), 0x00, calldatasize()) + sValue := keccak256(mload(0x40), calldatasize()) + sstore(result, sValue) + result := sValue + break + } + mstore(0x1f, sValue) + sValue := keccak256(0x20, 0x40) + sstore(result, sValue) + result := sValue + break + } + } + } + + /// @dev Returns a boolean with an approximately 1/n chance of being true. + /// This function may return a previously returned result. + function _randomChance(uint256 n) internal returns (bool result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + result := iszero(mod(r, n)) + } + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + /// This function may return a previously returned result. + function _randomPrivateKey() internal returns (uint256 result) { + result = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + for {} 1 {} { + if iszero(and(result, 0x10)) { + if iszero(and(result, 0x20)) { + result := add(and(result, 0xf), 1) + break + } + result := sub(_PRIVATE_KEY_MAX, and(result, 0xf)) + break + } + result := shr(1, result) + break + } + } + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + function _randomUniquePrivateKey(uint256 groupId) internal returns (uint256 result) { + result = _randomUniquePrivateKey(bytes32(groupId)); + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + function _randomUniquePrivateKey(bytes32 groupId) internal returns (uint256 result) { + do { + result = _randomPrivateKey(); + } while (__markAsGenerated("uint256", groupId, result)); + } + + /// @dev Returns a random private key that can be used for ECDSA signing. + function _randomUniquePrivateKey() internal returns (uint256 result) { + result = _randomUniquePrivateKey(""); + } + + /// @dev Private helper function to get the signer from a private key. + function __getSigner(uint256 privateKey) private view returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, 0xffa18649) // `addr(uint256)`. + mstore(0x20, privateKey) + result := mload(staticcall(gas(), _VM_ADDRESS, 0x1c, 0x24, 0x01, 0x20)) + } + } + + /// @dev Private helper to ensure an address is brutalized. + function __toBrutalizedAddress(address a) private pure returns (address result) { + /// @solidity memory-safe-assembly + assembly { + result := keccak256(0x00, 0x88) + result := xor(shl(160, xor(result, _ADDRESS_BRUTALIZER)), a) + mstore(0x10, result) + } + } + + /// @dev Private helper to ensure an address is brutalized. + function __toBrutalizedAddress(uint256 a) private pure returns (address result) { + /// @solidity memory-safe-assembly + assembly { + result := keccak256(0x00, 0x88) + result := xor(shl(160, xor(result, _ADDRESS_BRUTALIZER)), a) + mstore(0x10, result) + } + } + + /// @dev Returns a pseudorandom signer and its private key. + /// This function may return a previously returned result. + /// The signer may have dirty upper 96 bits. + function _randomSigner() internal returns (address signer, uint256 privateKey) { + privateKey = _randomPrivateKey(); + signer = __toBrutalizedAddress(__getSigner(privateKey)); + } + + /// @dev Returns a pseudorandom signer and its private key. + /// The signer may have dirty upper 96 bits. + function _randomUniqueSigner(uint256 groupId) internal returns (address signer, uint256 privateKey) { + (signer, privateKey) = _randomUniqueSigner(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom signer and its private key. + /// The signer may have dirty upper 96 bits. + function _randomUniqueSigner(bytes32 groupId) internal returns (address signer, uint256 privateKey) { + privateKey = _randomUniquePrivateKey(groupId); + signer = __toBrutalizedAddress(__getSigner(privateKey)); + } + + /// @dev Returns a pseudorandom signer and its private key. + /// The signer may have dirty upper 96 bits. + function _randomUniqueSigner() internal returns (address signer, uint256 privateKey) { + (signer, privateKey) = _randomUniqueSigner(""); + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + /// This function may return a previously returned result. + function _randomAddress() internal returns (address result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + result := xor(shl(158, r), and(sub(7, shr(252, r)), r)) + } + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueAddress(uint256 groupId) internal returns (address result) { + result = _randomUniqueAddress(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueAddress(bytes32 groupId) internal returns (address result) { + do { + result = _randomAddress(); + } while (__markAsGenerated("address", groupId, uint160(result))); + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueAddress() internal returns (address result) { + result = _randomUniqueAddress(""); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + /// This function may return a previously returned result. + function _randomNonZeroAddress() internal returns (address result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + result := xor(shl(158, r), and(sub(7, shr(252, r)), r)) + if iszero(shl(96, result)) { + mstore(0x00, result) + result := keccak256(0x00, 0x30) + } + } + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueNonZeroAddress(uint256 groupId) internal returns (address result) { + result = _randomUniqueNonZeroAddress(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueNonZeroAddress(bytes32 groupId) internal returns (address result) { + do { + result = _randomNonZeroAddress(); + } while (__markAsGenerated("address", groupId, uint160(result))); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + function _randomUniqueNonZeroAddress() internal returns (address result) { + result = _randomUniqueNonZeroAddress(""); + } + + /// @dev Cleans the upper 96 bits of the address. + /// This is included so that CI passes for older solc versions with --via-ir. + function _cleaned(address a) internal pure returns (address result) { + /// @solidity memory-safe-assembly + assembly { + result := shr(96, shl(96, a)) + } + } + + /// @dev Returns a pseudorandom address. + /// The result may have dirty upper 96 bits. + /// This function may return a previously returned result. + function _randomAddressWithVmVars() internal returns (address result) { + if (_randomChance(8)) result = __toBrutalizedAddress(_randomVmVar()); + else result = _randomAddress(); + } + + /// @dev Returns a pseudorandom non-zero address. + /// The result may have dirty upper 96 bits. + /// This function may return a previously returned result. + function _randomNonZeroAddressWithVmVars() internal returns (address result) { + do { + if (_randomChance(8)) result = __toBrutalizedAddress(_randomVmVar()); + else result = _randomAddress(); + } while (result == address(0)); + } + + /// @dev Returns a random variable in the virtual machine. + function _randomVmVar() internal returns (uint256 result) { + uint256 r = _randomUniform(); + uint256 t = r % 11; + if (t <= 4) { + if (t == 0) return uint160(address(this)); + if (t == 1) return uint160(tx.origin); + if (t == 2) return uint160(msg.sender); + if (t == 3) return uint160(_VM_ADDRESS); + if (t == 4) return uint160(0x000000000000000000636F6e736F6c652e6c6f67); + } + uint256 y = r >> 32; + if (t == 5) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, r) + codecopy(0x00, mod(and(y, 0xffff), add(codesize(), 0x20)), 0x20) + result := mload(0x00) + } + return result; + } + if (t == 6) { + /// @solidity memory-safe-assembly + assembly { + calldatacopy(0x00, mod(and(y, 0xffff), add(calldatasize(), 0x20)), 0x20) + result := mload(0x00) + } + return result; + } + if (t == 7) { + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) + returndatacopy(m, 0x00, returndatasize()) + result := mload(add(m, mod(and(y, 0xffff), add(returndatasize(), 0x20)))) + } + return result; + } + if (t == 8) { + /// @solidity memory-safe-assembly + assembly { + result := sload(and(y, 0xff)) + } + return result; + } + if (t == 9) { + /// @solidity memory-safe-assembly + assembly { + result := mload(mod(y, add(mload(0x40), 0x40))) + } + return result; + } + result = __getSigner(_randomPrivateKey()); + } + + /// @dev Returns a pseudorandom hashed address. + /// The result may have dirty upper 96 bits. + /// This function will not return an existing contract. + /// This function will not return a precompile address. + /// This function will not return a zero address. + /// This function may return a previously returned result. + function _randomHashedAddress() internal returns (address result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + mstore(0x1f, and(sub(7, shr(252, r)), r)) + calldatacopy(0x00, 0x00, 0x24) + result := keccak256(0x00, 0x3f) + } + } + + /// @dev Returns a pseudorandom address. + function _randomUniqueHashedAddress(uint256 groupId) internal returns (address result) { + result = _randomUniqueHashedAddress(bytes32(groupId)); + } + + /// @dev Returns a pseudorandom address. + function _randomUniqueHashedAddress(bytes32 groupId) internal returns (address result) { + do { + result = _randomHashedAddress(); + } while (__markAsGenerated("address", groupId, uint160(result))); + } + + /// @dev Returns a pseudorandom address. + function _randomUniqueHashedAddress() internal returns (address result) { + result = _randomUniqueHashedAddress(""); + } + + /// @dev Private helper function for returning random bytes. + function __randomBytes(bool zeroRightPad) private returns (bytes memory result) { + uint256 r = _randomUniform(); + /// @solidity memory-safe-assembly + assembly { + let n := and(r, 0x1ffff) + let t := shr(24, r) + for {} 1 {} { + // With a 1/256 chance, just return the zero pointer as the result. + if iszero(and(t, 0xff0)) { + result := 0x60 + break + } + result := mload(0x40) + // With a 15/16 chance, set the length to be + // exponentially distributed in the range [0,255] (inclusive). + if shr(252, r) { n := shr(and(t, 0x7), byte(5, r)) } + // Store some fixed word at the start of the string. + // We want this function to sometimes return duplicates. + mstore(add(result, 0x20), xor(calldataload(0x00), _TESTPLUS_RANDOMNESS_SLOT)) + // With a 1/2 chance, copy the contract code to the start and end. + if iszero(and(t, 0x1000)) { + // Copy to the start. + if iszero(and(t, 0x2000)) { codecopy(result, byte(1, r), codesize()) } + // Copy to the end. + codecopy(add(result, n), byte(2, r), 0x40) + } + // With a 1/16 chance, randomize the start and end. + if iszero(and(t, 0xf0000)) { + let y := mulmod(r, _LPRNG_MULTIPLIER, _LPRNG_MODULO) + mstore(add(result, 0x20), y) + mstore(add(result, n), xor(r, y)) + } + // With a 1/256 chance, make the result entirely zero bytes. + if iszero(byte(4, r)) { codecopy(result, codesize(), add(n, 0x20)) } + // Skip the zero-right-padding if not required. + if iszero(zeroRightPad) { + mstore(0x40, add(n, add(0x40, result))) // Allocate memory. + mstore(result, n) // Store the length. + break + } + mstore(add(add(result, 0x20), n), 0) // Zeroize the word after the result. + mstore(0x40, add(n, add(0x60, result))) // Allocate memory. + mstore(result, n) // Store the length. + break + } + } + } + + /// @dev Returns a random bytes string from 0 to 131071 bytes long. + /// This random bytes string may NOT be zero-right-padded. + /// This is intentional for memory robustness testing. + /// This function may return a previously returned result. + function _randomBytes() internal returns (bytes memory result) { + result = __randomBytes(false); + } + + /// @dev Returns a random bytes string from 0 to 131071 bytes long. + /// This function may return a previously returned result. + function _randomBytesZeroRightPadded() internal returns (bytes memory result) { + result = __randomBytes(true); + } + + /// @dev Truncate the bytes to `n` bytes. + /// Returns the result for function chaining. + function _truncateBytes(bytes memory b, uint256 n) internal pure returns (bytes memory result) { + /// @solidity memory-safe-assembly + assembly { + if gt(mload(b), n) { mstore(b, n) } + result := b + } + } + + /// @dev Returns the free memory pointer. + function _freeMemoryPointer() internal pure returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := mload(0x40) + } + } + + /// @dev Increments the free memory pointer by a world. + function _incrementFreeMemoryPointer() internal pure { + uint256 word = 0x20; + /// @solidity memory-safe-assembly + assembly { + mstore(0x40, add(mload(0x40), word)) + } + } + + /// @dev Adapted from `bound`: + /// https://github.com/foundry-rs/forge-std/blob/ff4bf7db008d096ea5a657f2c20516182252a3ed/src/StdUtils.sol#L10 + /// Differentially fuzzed tested against the original implementation. + function _hem(uint256 x, uint256 min, uint256 max) internal pure virtual returns (uint256 result) { + require(min <= max, "Max is less than min."); + /// @solidity memory-safe-assembly + assembly { + // prettier-ignore + for {} 1 {} { + // If `x` is between `min` and `max`, return `x` directly. + // This is to ensure that dictionary values + // do not get shifted if the min is nonzero. + // More info: https://github.com/foundry-rs/forge-std/issues/188 + if iszero(or(lt(x, min), gt(x, max))) { + result := x + break + } + let size := add(sub(max, min), 1) + if lt(gt(x, 3), gt(size, x)) { + result := add(min, x) + break + } + if lt(lt(x, not(3)), gt(size, not(x))) { + result := sub(max, not(x)) + break + } + // Otherwise, wrap x into the range [min, max], + // i.e. the range is inclusive. + if iszero(lt(x, max)) { + let d := sub(x, max) + let r := mod(d, size) + if iszero(r) { + result := max + break + } + result := sub(add(min, r), 1) + break + } + let d := sub(min, x) + let r := mod(d, size) + if iszero(r) { + result := min + break + } + result := add(sub(max, r), 1) + break + } + } + } + + /// @dev Deploys a contract via 0age's immutable create 2 factory for testing. + function _safeCreate2(uint256 payableAmount, bytes32 salt, bytes memory initializationCode) + internal + returns (address deploymentAddress) + { + // Canonical address of 0age's immutable create 2 factory. + address c2f = 0x0000000000FFe8B47B3e2130213B802212439497; + uint256 c2fCodeLength; + /// @solidity memory-safe-assembly + assembly { + c2fCodeLength := extcodesize(c2f) + } + if (c2fCodeLength == 0) { + bytes memory ic2fBytecode = + hex"60806040526004361061003f5760003560e01c806308508b8f1461004457806364e030871461009857806385cf97ab14610138578063a49a7c90146101bc575b600080fd5b34801561005057600080fd5b506100846004803603602081101561006757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101ec565b604080519115158252519081900360200190f35b61010f600480360360408110156100ae57600080fd5b813591908101906040810160208201356401000000008111156100d057600080fd5b8201836020820111156100e257600080fd5b8035906020019184600183028401116401000000008311171561010457600080fd5b509092509050610217565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561014457600080fd5b5061010f6004803603604081101561015b57600080fd5b8135919081019060408101602082013564010000000081111561017d57600080fd5b82018360208201111561018f57600080fd5b803590602001918460018302840111640100000000831117156101b157600080fd5b509092509050610592565b3480156101c857600080fd5b5061010f600480360360408110156101df57600080fd5b508035906020013561069e565b73ffffffffffffffffffffffffffffffffffffffff1660009081526020819052604090205460ff1690565b600083606081901c33148061024c57507fffffffffffffffffffffffffffffffffffffffff0000000000000000000000008116155b6102a1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260458152602001806107746045913960600191505060405180910390fd5b606084848080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920182905250604051855195965090943094508b93508692506020918201918291908401908083835b6020831061033557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016102f8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff018019909216911617905260408051929094018281037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00183528085528251928201929092207fff000000000000000000000000000000000000000000000000000000000000008383015260609890981b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016602183015260358201969096526055808201979097528251808203909701875260750182525084519484019490942073ffffffffffffffffffffffffffffffffffffffff81166000908152938490529390922054929350505060ff16156104a7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603f815260200180610735603f913960400191505060405180910390fd5b81602001825188818334f5955050508073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff161461053a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260468152602001806107b96046913960600191505060405180910390fd5b50505073ffffffffffffffffffffffffffffffffffffffff8116600090815260208190526040902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660011790559392505050565b6000308484846040516020018083838082843760408051919093018181037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001825280845281516020928301207fff000000000000000000000000000000000000000000000000000000000000008383015260609990991b7fffffffffffffffffffffffffffffffffffffffff000000000000000000000000166021820152603581019790975260558088019890985282518088039098018852607590960182525085519585019590952073ffffffffffffffffffffffffffffffffffffffff81166000908152948590529490932054939450505060ff909116159050610697575060005b9392505050565b604080517fff000000000000000000000000000000000000000000000000000000000000006020808301919091523060601b6021830152603582018590526055808301859052835180840390910181526075909201835281519181019190912073ffffffffffffffffffffffffffffffffffffffff81166000908152918290529190205460ff161561072e575060005b9291505056fe496e76616c696420636f6e7472616374206372656174696f6e202d20636f6e74726163742068617320616c7265616479206265656e206465706c6f7965642e496e76616c69642073616c74202d206669727374203230206279746573206f66207468652073616c74206d757374206d617463682063616c6c696e6720616464726573732e4661696c656420746f206465706c6f7920636f6e7472616374207573696e672070726f76696465642073616c7420616e6420696e697469616c697a6174696f6e20636f64652ea265627a7a723058202bdc55310d97c4088f18acf04253db593f0914059f0c781a9df3624dcef0d1cf64736f6c634300050a0032"; + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) + mstore(m, 0xb4d6c782) // `etch(address,bytes)`. + mstore(add(m, 0x20), c2f) + mstore(add(m, 0x40), 0x40) + let n := mload(ic2fBytecode) + mstore(add(m, 0x60), n) + for { let i := 0 } lt(i, n) { i := add(0x20, i) } { + mstore(add(add(m, 0x80), i), mload(add(add(ic2fBytecode, 0x20), i))) + } + pop(call(gas(), _VM_ADDRESS, 0, add(m, 0x1c), add(n, 0x64), 0x00, 0x00)) + } + } + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) + let n := mload(initializationCode) + mstore(m, 0x64e03087) // `safeCreate2(bytes32,bytes)`. + mstore(add(m, 0x20), salt) + mstore(add(m, 0x40), 0x40) + mstore(add(m, 0x60), n) + // prettier-ignore + for { let i := 0 } lt(i, n) { i := add(i, 0x20) } { + mstore(add(add(m, 0x80), i), mload(add(add(initializationCode, 0x20), i))) + } + if iszero(call(gas(), c2f, payableAmount, add(m, 0x1c), add(n, 0x64), m, 0x20)) { + returndatacopy(m, m, returndatasize()) + revert(m, returndatasize()) + } + deploymentAddress := mload(m) + } + } + + /// @dev Deploys a contract via 0age's immutable create 2 factory for testing. + function _safeCreate2(bytes32 salt, bytes memory initializationCode) internal returns (address deploymentAddress) { + deploymentAddress = _safeCreate2(0, salt, initializationCode); + } + + /// @dev This function will make forge's gas output display the approximate codesize of + /// the test contract as the amount of gas burnt. Useful for quick guess checking if + /// certain optimizations actually compiles to similar bytecode. + function test__codesize() external view { + /// @solidity memory-safe-assembly + assembly { + // If the caller is the contract itself (i.e. recursive call), burn all the gas. + if eq(caller(), address()) { invalid() } + mstore(0x00, 0xf09ff470) // Store the function selector of `test__codesize()`. + pop(staticcall(codesize(), address(), 0x1c, 0x04, 0x00, 0x00)) + } + } +}