From 7798d4a3638cc3972cbdad1e71d7c602d72106ac Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 09:34:53 -0600 Subject: [PATCH 01/28] Add SealedBidTokenSale contract and specification for a sealed-bid, multi-unit uniform-price token sale with USDC deposits, using OpenZeppelin's Ownable for ownership, Merkle-based token claims, and proceeds withdrawal to a treasury address. --- src/apps/SealedBidTokenSale.md | 212 +++++++++++++++++++++++++++++++ src/apps/SealedBidTokenSale.sol | 219 ++++++++++++++++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 src/apps/SealedBidTokenSale.md create mode 100644 src/apps/SealedBidTokenSale.sol diff --git a/src/apps/SealedBidTokenSale.md b/src/apps/SealedBidTokenSale.md new file mode 100644 index 00000000..9e1fcee9 --- /dev/null +++ b/src/apps/SealedBidTokenSale.md @@ -0,0 +1,212 @@ +# Updated Technical Specification + +Below is the revised **technical specification** for a Solidity **sealed‐bid, multi‐unit uniform‐price** token sale contract. The specification reflects the latest changes: + +1. **Use of OpenZeppelin’s `Ownable`** for ownership logic. +2. **Withdrawal of proceeds** goes to a **predetermined treasury address**. + +All other core features—time‐limited deposits in USDC, minimum/maximum cap checks, finalization logic, off‐chain price discovery, Merkle‐based token claims, and refunds if the sale fails—remain consistent with the previous design. + +--- + +## 1. **Contract Overview** + +- **Name**: `SealedBidTokenSale` +- **Purpose**: Accept USDC deposits for an off‐chain price discovery token sale, enforce timing and caps, enable refunds if the sale is unsuccessful, and distribute tokens (via a Merkle proof) if successful. + +--- + +## 2. **Key Roles** + +- **Owner**: + - Inherits from OpenZeppelin `Ownable`. + - The owner sets crucial parameters during contract deployment. + - The owner finalizes the sale, sets the Merkle root, and withdraws proceeds to the treasury address. + +- **Participants**: + - Deposit USDC during the sale window. + - Withdraw their deposit if the sale fails. + - Claim tokens after the sale succeeds, using a Merkle proof. + +- **Treasury**: + - A predetermined address specified at contract deployment. + - Receives the USDC proceeds if the sale succeeds. + +--- + +## 3. **Immutable & Configurable Parameters** + +1. **`Ownable`** + - The contract is governed by an owner (`owner()`) managed by OpenZeppelin’s `Ownable`. + +2. **`treasury`** (`address`) + - Set at construction. + - **Immutable**; where funds are sent on success. + +3. **`usdcToken`** (`IERC20`) + - The address of the USDC contract. + - Used for `transferFrom` and `transfer` calls. + +4. **`startTime`** and **`endTime`** (`uint256`) + - The sale window during which deposits are accepted. + +5. **`minimumCap`** (`uint256`) + - The minimum total USDC deposit threshold required for the sale to succeed. + +6. **`maximumCap`** (`uint256`) + - The maximum total USDC deposit allowed; can be `0` if no maximum limit is needed. + +--- + +## 4. **State Variables** + +1. **`finalized`** (`bool`) + - Indicates if the sale has been finalized. + - Once set, the contract’s deposit logic is locked. + +2. **`successful`** (`bool`) + - `true` if `totalDeposited >= minimumCap` upon finalization. + - Determines whether users can claim tokens or must withdraw refunds. + +3. **`totalDeposited`** (`uint256`) + - Running total of all USDC deposits received. + +4. **`deposits`** (`mapping(address => uint256)`) + - Tracks each participant’s cumulative deposit. + +5. **`merkleRoot`** (`bytes32`) + - Root hash of an off‐chain Merkle tree that encodes final token allocations. + +6. **`hasClaimed`** (`mapping(address => bool)`) + - Tracks whether a participant has already claimed tokens. + +--- + +## 5. **Core Functions** + +### 5.1 **`deposit(uint256 amount)`** +- **Purpose**: Allows participants to deposit USDC into the sale, multiple times if desired. +- **Constraints**: + 1. Must be called after `startTime` and before `endTime`. + 2. Sale must not be `finalized`. + 3. The `amount` must be non-zero. + 4. If `maximumCap > 0`, `totalDeposited + amount <= maximumCap`. + 5. Transfers USDC using `transferFrom(msg.sender, address(this), amount)`. +- **Effects**: + - Increments `deposits[msg.sender]` and `totalDeposited`. + - Emits a `Deposit` event. + +### 5.2 **`withdraw()`** +- **Purpose**: Allows participants to **refund** their USDC deposits if the sale fails. +- **Constraints**: + 1. Can only be called after `finalized`. + 2. Only possible if `successful == false`. + 3. Caller’s `deposits[msg.sender]` must be > 0. +- **Effects**: + - Refunds the user’s entire deposit via `transfer`. + - Sets `deposits[msg.sender] = 0` to prevent re‐entrancy. + - Emits a `Withdraw` event. + +### 5.3 **`finalize()`** (Owner‐only) +- **Purpose**: Ends the deposit phase, locks in whether the sale is successful, and stops further deposits. +- **Constraints**: + 1. Can only be called by the **owner**. + 2. Must not be already `finalized`. + 3. Typically requires current time >= `endTime` **or** `totalDeposited == maximumCap` (if the cap is exhausted early). +- **Effects**: + - Sets `finalized = true`. + - Sets `successful = (totalDeposited >= minimumCap)`. + - Emits a `Finalized` event with the final outcome. + +### 5.4 **`setMerkleRoot(bytes32 _merkleRoot)`** (Owner‐only) +- **Purpose**: Records the final allocations in a **Merkle root** for off‐chain computed distribution. +- **Constraints**: + 1. Must be called by the **owner**. + 2. The sale must be `finalized` and `successful`. +- **Effects**: + - Updates `merkleRoot` to `_merkleRoot`. + - Emits a `MerkleRootSet` event. + +### 5.5 **`claimTokens(uint256 allocation, bytes32[] calldata proof)`** +- **Purpose**: Lets each participant claim their allocated tokens (as computed off‐chain), verified by a **Merkle proof**. +- **Constraints**: + 1. The sale must be `finalized` and `successful`. + 2. A valid `merkleRoot` must be set. + 3. `hasClaimed[msg.sender] == false` (no double‐claim). + 4. The `(address, allocation)` leaf must be verified against `merkleRoot` using `MerkleProof.verify`. +- **Effects**: + - Marks `hasClaimed[msg.sender] = true`. + - **Transfers** (or **mints**) `allocation` tokens to the caller. + - Emits a `Claim` event. + +### 5.6 **`withdrawProceeds()`** (Owner‐only) +- **Purpose**: Transfers **all** USDC proceeds to the **predetermined `treasury`** address if the sale is successful. +- **Constraints**: + 1. Must be called by the **owner**. + 2. The sale must be `finalized` and `successful`. +- **Effects**: + - Transfers the entire USDC balance from the contract to `treasury`. + +--- + +## 6. **Life Cycle** + +1. **Deployment** + - Deployed with constructor parameters, including `treasury`, `startTime`, `endTime`, `minimumCap`, `maximumCap`. + - The contract references the USDC address for deposits. + +2. **Deposit Phase** + - Participants call `deposit(amount)` any number of times from `startTime` to `endTime` (unless `maximumCap` is reached). + - `totalDeposited` is aggregated. + +3. **Finalization** + - After `endTime` (or upon reaching `maximumCap`), the owner calls `finalize()`. + - The contract determines `successful` based on `minimumCap`. + +4. **Outcomes** + - **Unsuccessful**: If `totalDeposited < minimumCap`, participants can call `withdraw()` to get refunds. + - **Successful**: + - The owner sets a `merkleRoot` to define each user’s final token allocation. + - Participants use `claimTokens(allocation, proof)` to claim tokens. + - The owner can call `withdrawProceeds()` to send USDC to the `treasury`. + +--- + +## 7. **Implementation Considerations** + +1. **Token Distribution Mechanism** + - The contract must hold or be able to mint the tokens for `claimTokens()`. + - This might involve transferring tokens in advance or using a mint function in an external token contract. + +2. **Security** + - Use **OpenZeppelin** libraries (`Ownable`, `ReentrancyGuard`, `MerkleProof`) for best practices. + - Validate deposit calls to prevent deposits outside the allowed window. + - Carefully handle refunds (set user deposit to 0 before transferring USDC back). + +3. **Edge Cases** + - If `maximumCap == 0`, only time gating applies. + - If participants deposit after `maximumCap` is reached, the contract must revert. + - The owner might finalize **early** if `totalDeposited == maximumCap` before `endTime`. + - If `startTime` equals `endTime` or if `_endTime <= _startTime`, the constructor should revert. + +4. **No Immediate Secondary Trading** + - The specification assumes tokens are **not** tradable until after the sale. + - Participants may hold or wait to claim tokens; however, that is outside the core on‐chain deposit/refund logic. + +5. **Custom Errors** + - Reverts use 0.8‐style **custom errors** for gas efficiency (e.g., `error SaleNotStarted();`, `error SaleEnded();`, etc.). + +--- + +## 8. **Final Takeaway** + +This specification establishes a **time‐bound, USDC‐based deposit system** with: + +- A **minimum funding threshold** (`minimumCap`) for success. +- An **optional maximum** (`maximumCap`). +- **Finalization** by the owner. +- **Refunds** if not successful. +- **Merkle‐based claims** if successful. +- **Proceeds** withdrawn by the owner to a **fixed `treasury` address**. + +All **off‐chain** bid details and final allocation logic remain external; the contract only enforces **deposits**, **caps**, **timing**, and **fund distribution**, while using a **Merkle tree** for post‐sale token allocation. diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol new file mode 100644 index 00000000..a358ad4a --- /dev/null +++ b/src/apps/SealedBidTokenSale.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +/** + * @title SealedBidTokenSale + * @dev A contract to accept USDC deposits for a sealed-bid, multi-unit uniform-price sale + * with off-chain price discovery. Participants can deposit multiple times, and if the + * sale meets a minimumCap, it is deemed successful. A Merkle root is published later + * for token claims. Depositors can withdraw funds if the sale fails. + * + * Proceeds are withdrawn to a predetermined treasury address (set in the constructor). + */ +contract SealedBidTokenSale is Ownable, ReentrancyGuard { + // ----------------------------------------------------------------------- + // Errors in 0.8 style (custom errors) + // ----------------------------------------------------------------------- + error SaleNotStarted(); + error SaleEnded(); + error AlreadyFinalized(); + error NotFinalized(); + error SaleNotEnded(); + error SaleNotSuccessful(); + error SaleWasSuccessful(); + error ZeroDeposit(); + error ExceedsMaximumCap(); + error NothingToWithdraw(); + error AlreadyClaimed(); + error InvalidProof(); + error MerkleRootNotSet(); + + // ----------------------------------------------------------------------- + // Events + // ----------------------------------------------------------------------- + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + event Finalized(bool successful, uint256 totalDeposited); + event MerkleRootSet(bytes32 root); + event Claim(address indexed user, uint256 tokenAmount); + + // ----------------------------------------------------------------------- + // Immutable & Configurable Parameters + // ----------------------------------------------------------------------- + IERC20 public immutable usdcToken; // Address of USDC token contract + IERC20 public immutable saleToken; + address public immutable treasury; // Where proceeds go upon withdrawal + + uint256 public immutable startTime; // Sale start timestamp + uint256 public immutable endTime; // Sale end timestamp + uint256 public immutable minimumCap; // Minimum USDC needed for a successful sale + uint256 public immutable maximumCap; // Maximum USDC deposit allowed (0 if none) + + // ----------------------------------------------------------------------- + // State Variables + // ----------------------------------------------------------------------- + bool public finalized; // Whether sale has been finalized + bool public successful; // Whether sale is successful + uint256 public totalDeposited; // Total USDC deposited by all participants + + // user => total USDC deposited + mapping(address => uint256) public deposits; + + // Merkle root for final token allocations (off-chain computed) + bytes32 public merkleRoot; + // Tracks whether a user has claimed their tokens + mapping(address => bool) public hasClaimed; + + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + /** + * @param _treasury The address to which proceeds will be sent after a successful sale + * @param _usdcToken The USDC token contract address + * @param _startTime The block timestamp when deposits can begin + * @param _endTime The block timestamp when deposits end + * @param _minimumCap The minimum USDC deposit needed for the sale to succeed + * @param _maximumCap The maximum USDC deposit allowed (use 0 if no max) + */ + constructor( + address _treasury, + IERC20 _usdcToken, + uint256 _startTime, + uint256 _endTime, + uint256 _minimumCap, + uint256 _maximumCap + ) { + require(_treasury != address(0), "treasury cannot be zero address"); + require(_endTime > _startTime, "endTime must be after startTime"); + + treasury = _treasury; + usdcToken = _usdcToken; + startTime = _startTime; + endTime = _endTime; + minimumCap = _minimumCap; + maximumCap = _maximumCap; + } + + // ----------------------------------------------------------------------- + // Public (User) Functions + // ----------------------------------------------------------------------- + + /** + * @notice Deposit USDC into the token sale. Must be within sale time window. + * @param amount The amount of USDC to deposit + */ + function deposit(uint256 amount) external nonReentrant { + if (block.timestamp < startTime) revert SaleNotStarted(); + if (block.timestamp > endTime) revert SaleEnded(); + if (finalized) revert AlreadyFinalized(); + if (amount == 0) revert ZeroDeposit(); + + // Check maximum cap if applicable + if ((totalDeposited + amount) > maximumCap) { + revert ExceedsMaximumCap(); + } + + // Transfer USDC from user to this contract + bool successTransfer = usdcToken.transferFrom(msg.sender, address(this), amount); + require(successTransfer, "USDC transfer failed"); + + // Update state + deposits[msg.sender] += amount; + totalDeposited += amount; + + emit Deposit(msg.sender, amount); + } + + /** + * @notice Withdraw your USDC if the sale is not successful (i.e., below minimumCap). + * Callable after finalization if `successful == false`. + */ + function withdraw() external nonReentrant { + if (!finalized) revert NotFinalized(); + if (successful) revert SaleWasSuccessful(); + + uint256 userDeposit = deposits[msg.sender]; + if (userDeposit == 0) revert NothingToWithdraw(); + + // Zero out deposit before transferring + deposits[msg.sender] = 0; + + bool successTransfer = usdcToken.transfer(msg.sender, userDeposit); + require(successTransfer, "USDC transfer failed"); + + emit Withdraw(msg.sender, userDeposit); + } + + /** + * @notice Claim your allocated tokens after the sale is finalized and successful. + * Requires a valid Merkle proof of (address, allocation). + * @param allocation The total token amount allocated to msg.sender + * @param proof The Merkle proof for (msg.sender, allocation) + */ + function claimTokens(uint256 allocation, bytes32[] calldata proof) external nonReentrant { + if (!finalized || !successful) revert SaleNotSuccessful(); + if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); + if (hasClaimed[msg.sender]) revert AlreadyClaimed(); + + // Verify Merkle proof + bytes32 leaf = keccak256(abi.encodePacked(msg.sender, allocation)); + bool valid = MerkleProof.verify(proof, merkleRoot, leaf); + if (!valid) revert InvalidProof(); + + // Mark as claimed + hasClaimed[msg.sender] = true; + + // --- Token Transfer Logic --- + saleToken.transfer(msg.sender, allocation); + + emit Claim(msg.sender, allocation); + } + + // ----------------------------------------------------------------------- + // Owner Functions + // ----------------------------------------------------------------------- + + /** + * @notice Finalize the sale after endTime (or earlier if max cap reached). + * Determines if it's successful and stops further deposits. + */ + function finalize() external onlyOwner { + if (finalized) revert AlreadyFinalized(); + + if (block.timestamp < endTime && !(totalDeposited == maximumCap)) { + revert SaleNotEnded(); + } + + finalized = true; + successful = (totalDeposited >= minimumCap); + + emit Finalized(successful, totalDeposited); + } + + /** + * @notice Set the Merkle root that represents each user's final allocation. + * Can only be set after the sale is finalized and successful. + * @param _merkleRoot The root of the Merkle tree + */ + function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner { + if (!finalized || !successful) revert SaleNotSuccessful(); + merkleRoot = _merkleRoot; + emit MerkleRootSet(_merkleRoot); + } + + /** + * @notice Withdraw the USDC proceeds to the treasury address (if sale is successful). + * Owner can call this at any time after successful finalization. + */ + function withdrawProceeds() external onlyOwner { + if (!finalized || !successful) revert SaleNotSuccessful(); + uint256 balance = usdcToken.balanceOf(address(this)); + bool successTransfer = usdcToken.transfer(treasury, balance); + require(successTransfer, "USDC transfer failed"); + } +} From c07793d3d8ae75e9e5991b80c1885d095c1cee8c Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 09:41:24 -0600 Subject: [PATCH 02/28] Remove outdated ownership and treasury details from SealedBidTokenSale specification for clarity. --- src/apps/SealedBidTokenSale.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/apps/SealedBidTokenSale.md b/src/apps/SealedBidTokenSale.md index 9e1fcee9..c8de2233 100644 --- a/src/apps/SealedBidTokenSale.md +++ b/src/apps/SealedBidTokenSale.md @@ -1,11 +1,8 @@ -# Updated Technical Specification +# SealedBidTokenSale Technical Specification -Below is the revised **technical specification** for a Solidity **sealed‐bid, multi‐unit uniform‐price** token sale contract. The specification reflects the latest changes: +Below is the **technical specification** for a Solidity **sealed‐bid, multi‐unit uniform‐price** token sale contract. -1. **Use of OpenZeppelin’s `Ownable`** for ownership logic. -2. **Withdrawal of proceeds** goes to a **predetermined treasury address**. - -All other core features—time‐limited deposits in USDC, minimum/maximum cap checks, finalization logic, off‐chain price discovery, Merkle‐based token claims, and refunds if the sale fails—remain consistent with the previous design. +All other core features—time‐limited deposits in USDC, minimum/maximum cap checks, finalization logic, off‐chain price discovery, Merkle‐based token claims, and refunds if the sale fails—remain consistent with the design. --- From 7564bfca444442c76fa52fa71ac1c3f75f317da6 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 16:57:57 -0600 Subject: [PATCH 03/28] Remove maximum cap check and associated error handling from SealedBidTokenSale contract. --- src/apps/SealedBidTokenSale.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index a358ad4a..7185b392 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -27,7 +27,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error SaleNotSuccessful(); error SaleWasSuccessful(); error ZeroDeposit(); - error ExceedsMaximumCap(); error NothingToWithdraw(); error AlreadyClaimed(); error InvalidProof(); @@ -113,11 +112,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { if (finalized) revert AlreadyFinalized(); if (amount == 0) revert ZeroDeposit(); - // Check maximum cap if applicable - if ((totalDeposited + amount) > maximumCap) { - revert ExceedsMaximumCap(); - } - // Transfer USDC from user to this contract bool successTransfer = usdcToken.transferFrom(msg.sender, address(this), amount); require(successTransfer, "USDC transfer failed"); From a6c2da70a5dcf5a751e6434e1bfcd59a29859827 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 17:39:13 -0600 Subject: [PATCH 04/28] Refactor SealedBidTokenSale contract: update OpenZeppelin imports, use SafeERC20 for USDC transfers, and rename usdcToken to USDC for clarity. --- src/apps/SealedBidTokenSale.sol | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 7185b392..ce2b2fd9 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {Ownable} from "@openzeppelin-5.0.1/contracts/access/Ownable.sol"; +import {IERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin-5.0.1/contracts/utils/ReentrancyGuard.sol"; +import {MerkleProof} from "@openzeppelin-5.0.1/contracts/utils/cryptography/MerkleProof.sol"; +import {SafeERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title SealedBidTokenSale @@ -16,6 +17,8 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; * Proceeds are withdrawn to a predetermined treasury address (set in the constructor). */ contract SealedBidTokenSale is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + // ----------------------------------------------------------------------- // Errors in 0.8 style (custom errors) // ----------------------------------------------------------------------- @@ -44,7 +47,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { // ----------------------------------------------------------------------- // Immutable & Configurable Parameters // ----------------------------------------------------------------------- - IERC20 public immutable usdcToken; // Address of USDC token contract + IERC20 public immutable USDC; // Address of USDC token contract IERC20 public immutable saleToken; address public immutable treasury; // Where proceeds go upon withdrawal @@ -86,12 +89,12 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { uint256 _endTime, uint256 _minimumCap, uint256 _maximumCap - ) { + ) Ownable(msg.sender) { require(_treasury != address(0), "treasury cannot be zero address"); require(_endTime > _startTime, "endTime must be after startTime"); treasury = _treasury; - usdcToken = _usdcToken; + USDC = _usdcToken; startTime = _startTime; endTime = _endTime; minimumCap = _minimumCap; @@ -113,8 +116,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { if (amount == 0) revert ZeroDeposit(); // Transfer USDC from user to this contract - bool successTransfer = usdcToken.transferFrom(msg.sender, address(this), amount); - require(successTransfer, "USDC transfer failed"); + USDC.safeTransferFrom(msg.sender, address(this), amount); // Update state deposits[msg.sender] += amount; @@ -137,7 +139,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { // Zero out deposit before transferring deposits[msg.sender] = 0; - bool successTransfer = usdcToken.transfer(msg.sender, userDeposit); + bool successTransfer = USDC.transfer(msg.sender, userDeposit); require(successTransfer, "USDC transfer failed"); emit Withdraw(msg.sender, userDeposit); @@ -206,8 +208,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { */ function withdrawProceeds() external onlyOwner { if (!finalized || !successful) revert SaleNotSuccessful(); - uint256 balance = usdcToken.balanceOf(address(this)); - bool successTransfer = usdcToken.transfer(treasury, balance); - require(successTransfer, "USDC transfer failed"); + uint256 balance = USDC.balanceOf(address(this)); + USDC.transfer(treasury, balance); } } From 57a28de80258b85f10aead70219c2bf7e0692f4e Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 17:44:40 -0600 Subject: [PATCH 05/28] Refactor SealedBidTokenSale constructor to use custom errors for invalid treasury address and end --- src/apps/SealedBidTokenSale.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index ce2b2fd9..8538002d 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -34,6 +34,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error AlreadyClaimed(); error InvalidProof(); error MerkleRootNotSet(); + error InvalidTreasuryAddress(); + error InvalidEndTime(); // ----------------------------------------------------------------------- // Events @@ -90,8 +92,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { uint256 _minimumCap, uint256 _maximumCap ) Ownable(msg.sender) { - require(_treasury != address(0), "treasury cannot be zero address"); - require(_endTime > _startTime, "endTime must be after startTime"); + if (_treasury == address(0)) revert InvalidTreasuryAddress(); + if (_endTime <= _startTime) revert InvalidEndTime(); treasury = _treasury; USDC = _usdcToken; From 019d61b4c1b8792e45585fa7d123798f78308d77 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 17:45:53 -0600 Subject: [PATCH 06/28] Refactor SealedBidTokenSale contract: enhance error handling, update event emissions, improve function documentation, and streamline token transfer logic. --- src/apps/SealedBidTokenSale.sol | 196 ++++++++++++++++---------------- 1 file changed, 95 insertions(+), 101 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 8538002d..64b8a627 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -9,80 +9,96 @@ import {SafeERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/utils/SafeERC /** * @title SealedBidTokenSale - * @dev A contract to accept USDC deposits for a sealed-bid, multi-unit uniform-price sale - * with off-chain price discovery. Participants can deposit multiple times, and if the - * sale meets a minimumCap, it is deemed successful. A Merkle root is published later - * for token claims. Depositors can withdraw funds if the sale fails. - * - * Proceeds are withdrawn to a predetermined treasury address (set in the constructor). + * @dev Sealed-bid auction-style token sale with USDC deposits and Merkle-based claims. + * Features time-bound participation, minimum/maximum caps, and non-custodial withdrawals. */ contract SealedBidTokenSale is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; - // ----------------------------------------------------------------------- - // Errors in 0.8 style (custom errors) - // ----------------------------------------------------------------------- - error SaleNotStarted(); - error SaleEnded(); + /* ============ Custom Errors ============ */ + + /// @notice Invalid treasury address provided (zero address) + error InvalidTreasuryAddress(address treasury); + /// @notice End time must be after start time + error InvalidEndTime(uint256 startTime, uint256 endTime); + /// @notice Sale period has not started yet + error SaleNotStarted(uint256 currentTime, uint256 startTime); + /// @notice Sale period has already ended + error SaleEnded(uint256 currentTime, uint256 endTime); + /// @notice Sale has already been finalized error AlreadyFinalized(); + /// @notice Action requires prior finalization error NotFinalized(); - error SaleNotEnded(); + /// @notice Sale period has not ended yet + error SaleNotEnded(uint256 currentTime, uint256 endTime); + /// @notice Operation requires successful sale state error SaleNotSuccessful(); + /// @notice Operation requires failed sale state error SaleWasSuccessful(); + /// @notice Deposit amount must be greater than zero error ZeroDeposit(); - error NothingToWithdraw(); - error AlreadyClaimed(); + /// @notice No funds available for withdrawal + error NothingToWithdraw(address user); + /// @notice Tokens have already been claimed + error AlreadyClaimed(address user); + /// @notice Provided Merkle proof is invalid error InvalidProof(); + /// @notice Merkle root not set for claims error MerkleRootNotSet(); - error InvalidTreasuryAddress(); - error InvalidEndTime(); - // ----------------------------------------------------------------------- - // Events - // ----------------------------------------------------------------------- + /* ============ Events ============ */ + + /// @notice Emitted on USDC deposit event Deposit(address indexed user, uint256 amount); + /// @notice Emitted on USDC withdrawal event Withdraw(address indexed user, uint256 amount); + /// @notice Emitted when sale is finalized event Finalized(bool successful, uint256 totalDeposited); + /// @notice Emitted when Merkle root is set event MerkleRootSet(bytes32 root); + /// @notice Emitted on successful token claim event Claim(address indexed user, uint256 tokenAmount); - // ----------------------------------------------------------------------- - // Immutable & Configurable Parameters - // ----------------------------------------------------------------------- - IERC20 public immutable USDC; // Address of USDC token contract - IERC20 public immutable saleToken; - address public immutable treasury; // Where proceeds go upon withdrawal - - uint256 public immutable startTime; // Sale start timestamp - uint256 public immutable endTime; // Sale end timestamp - uint256 public immutable minimumCap; // Minimum USDC needed for a successful sale - uint256 public immutable maximumCap; // Maximum USDC deposit allowed (0 if none) - - // ----------------------------------------------------------------------- - // State Variables - // ----------------------------------------------------------------------- - bool public finalized; // Whether sale has been finalized - bool public successful; // Whether sale is successful - uint256 public totalDeposited; // Total USDC deposited by all participants - - // user => total USDC deposited - mapping(address => uint256) public deposits; - - // Merkle root for final token allocations (off-chain computed) + /* ============ Immutable Parameters ============ */ + + /// @notice USDC token contract + IERC20 public immutable USDC; + /// @notice Treasury address for proceeds + address public immutable treasury; + /// @notice Sale start timestamp + uint256 public immutable startTime; + /// @notice Sale end timestamp + uint256 public immutable endTime; + /// @notice Minimum USDC required for success + uint256 public immutable minimumCap; + /// @notice Maximum USDC allowed in sale + uint256 public immutable maximumCap; + + /* ============ State Variables ============ */ + + /// @notice Sale finalization status + bool public finalized; + /// @notice Sale success status + bool public successful; + /// @notice Total USDC deposited + uint256 public totalDeposited; + /// @notice Merkle root for allocations bytes32 public merkleRoot; - // Tracks whether a user has claimed their tokens + /// @notice User deposits tracking + mapping(address => uint256) public deposits; + /// @notice Claims tracking mapping(address => bool) public hasClaimed; - // ----------------------------------------------------------------------- - // Constructor - // ----------------------------------------------------------------------- + /* ============ Constructor ============ */ + /** - * @param _treasury The address to which proceeds will be sent after a successful sale - * @param _usdcToken The USDC token contract address - * @param _startTime The block timestamp when deposits can begin - * @param _endTime The block timestamp when deposits end - * @param _minimumCap The minimum USDC deposit needed for the sale to succeed - * @param _maximumCap The maximum USDC deposit allowed (use 0 if no max) + * @notice Initialize sale parameters + * @param _treasury Treasury address for proceeds + * @param _usdcToken USDC token address + * @param _startTime Sale start timestamp + * @param _endTime Sale end timestamp + * @param _minimumCap Minimum USDC for success + * @param _maximumCap Maximum USDC allowed */ constructor( address _treasury, @@ -92,8 +108,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { uint256 _minimumCap, uint256 _maximumCap ) Ownable(msg.sender) { - if (_treasury == address(0)) revert InvalidTreasuryAddress(); - if (_endTime <= _startTime) revert InvalidEndTime(); + if (_treasury == address(0)) revert InvalidTreasuryAddress(_treasury); + if (_endTime <= _startTime) revert InvalidEndTime(_startTime, _endTime); treasury = _treasury; USDC = _usdcToken; @@ -103,24 +119,19 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { maximumCap = _maximumCap; } - // ----------------------------------------------------------------------- - // Public (User) Functions - // ----------------------------------------------------------------------- + /* ============ User Functions ============ */ /** - * @notice Deposit USDC into the token sale. Must be within sale time window. - * @param amount The amount of USDC to deposit + * @notice Deposit USDC into the sale + * @param amount Amount of USDC to deposit */ function deposit(uint256 amount) external nonReentrant { - if (block.timestamp < startTime) revert SaleNotStarted(); - if (block.timestamp > endTime) revert SaleEnded(); + if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); + if (block.timestamp > endTime) revert SaleEnded(block.timestamp, endTime); if (finalized) revert AlreadyFinalized(); if (amount == 0) revert ZeroDeposit(); - // Transfer USDC from user to this contract USDC.safeTransferFrom(msg.sender, address(this), amount); - - // Update state deposits[msg.sender] += amount; totalDeposited += amount; @@ -128,75 +139,60 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { } /** - * @notice Withdraw your USDC if the sale is not successful (i.e., below minimumCap). - * Callable after finalization if `successful == false`. + * @notice Withdraw USDC if sale failed */ function withdraw() external nonReentrant { if (!finalized) revert NotFinalized(); if (successful) revert SaleWasSuccessful(); - uint256 userDeposit = deposits[msg.sender]; - if (userDeposit == 0) revert NothingToWithdraw(); + uint256 amount = deposits[msg.sender]; + if (amount == 0) revert NothingToWithdraw(msg.sender); - // Zero out deposit before transferring deposits[msg.sender] = 0; + USDC.safeTransfer(msg.sender, amount); - bool successTransfer = USDC.transfer(msg.sender, userDeposit); - require(successTransfer, "USDC transfer failed"); - - emit Withdraw(msg.sender, userDeposit); + emit Withdraw(msg.sender, amount); } /** - * @notice Claim your allocated tokens after the sale is finalized and successful. - * Requires a valid Merkle proof of (address, allocation). - * @param allocation The total token amount allocated to msg.sender - * @param proof The Merkle proof for (msg.sender, allocation) + * @notice Claim allocated tokens using Merkle proof + * @param allocation Token amount allocated to sender + * @param proof Merkle proof for allocation */ function claimTokens(uint256 allocation, bytes32[] calldata proof) external nonReentrant { if (!finalized || !successful) revert SaleNotSuccessful(); if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); - if (hasClaimed[msg.sender]) revert AlreadyClaimed(); + if (hasClaimed[msg.sender]) revert AlreadyClaimed(msg.sender); - // Verify Merkle proof bytes32 leaf = keccak256(abi.encodePacked(msg.sender, allocation)); - bool valid = MerkleProof.verify(proof, merkleRoot, leaf); - if (!valid) revert InvalidProof(); + if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof(); - // Mark as claimed hasClaimed[msg.sender] = true; - - // --- Token Transfer Logic --- - saleToken.transfer(msg.sender, allocation); + // saleToken.safeTransfer(msg.sender, allocation); // Requires saleToken initialization emit Claim(msg.sender, allocation); } - // ----------------------------------------------------------------------- - // Owner Functions - // ----------------------------------------------------------------------- + /* ============ Admin Functions ============ */ /** - * @notice Finalize the sale after endTime (or earlier if max cap reached). - * Determines if it's successful and stops further deposits. + * @notice Finalize sale outcome */ function finalize() external onlyOwner { if (finalized) revert AlreadyFinalized(); - - if (block.timestamp < endTime && !(totalDeposited == maximumCap)) { - revert SaleNotEnded(); + if (block.timestamp < endTime && totalDeposited != maximumCap) { + revert SaleNotEnded(block.timestamp, endTime); } finalized = true; - successful = (totalDeposited >= minimumCap); + successful = totalDeposited >= minimumCap; emit Finalized(successful, totalDeposited); } /** - * @notice Set the Merkle root that represents each user's final allocation. - * Can only be set after the sale is finalized and successful. - * @param _merkleRoot The root of the Merkle tree + * @notice Set Merkle root for allocations + * @param _merkleRoot Root of allocation Merkle tree */ function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner { if (!finalized || !successful) revert SaleNotSuccessful(); @@ -205,12 +201,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { } /** - * @notice Withdraw the USDC proceeds to the treasury address (if sale is successful). - * Owner can call this at any time after successful finalization. + * @notice Withdraw proceeds to treasury */ function withdrawProceeds() external onlyOwner { if (!finalized || !successful) revert SaleNotSuccessful(); - uint256 balance = USDC.balanceOf(address(this)); - USDC.transfer(treasury, balance); + USDC.safeTransfer(treasury, USDC.balanceOf(address(this))); } } From 32dbb38f7dfda76114000797410e90c461b31101 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 17:46:12 -0600 Subject: [PATCH 07/28] Remove unnecessary whitespace in SealedBidTokenSale.sol for improved code readability. --- src/apps/SealedBidTokenSale.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 64b8a627..5a5572e2 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -16,7 +16,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; /* ============ Custom Errors ============ */ - + /// @notice Invalid treasury address provided (zero address) error InvalidTreasuryAddress(address treasury); /// @notice End time must be after start time @@ -47,7 +47,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error MerkleRootNotSet(); /* ============ Events ============ */ - + /// @notice Emitted on USDC deposit event Deposit(address indexed user, uint256 amount); /// @notice Emitted on USDC withdrawal @@ -60,7 +60,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { event Claim(address indexed user, uint256 tokenAmount); /* ============ Immutable Parameters ============ */ - + /// @notice USDC token contract IERC20 public immutable USDC; /// @notice Treasury address for proceeds @@ -75,7 +75,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { uint256 public immutable maximumCap; /* ============ State Variables ============ */ - + /// @notice Sale finalization status bool public finalized; /// @notice Sale success status From f4ef09580b11d67ee627cdbaa53afe4dd4a31714 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Wed, 29 Jan 2025 17:49:30 -0600 Subject: [PATCH 08/28] Add sale token address validation and initialization in SealedBidTokenSale contract. --- src/apps/SealedBidTokenSale.sol | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 5a5572e2..98d1da8e 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -17,6 +17,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ Custom Errors ============ */ + /// @notice Invalid sale token address provided (zero address) + error InvalidSaleTokenAddress(address token); /// @notice Invalid treasury address provided (zero address) error InvalidTreasuryAddress(address treasury); /// @notice End time must be after start time @@ -61,6 +63,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ Immutable Parameters ============ */ + /// @notice Sale token contract + IERC20 public immutable saleToken; /// @notice USDC token contract IERC20 public immutable USDC; /// @notice Treasury address for proceeds @@ -93,6 +97,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /** * @notice Initialize sale parameters + * @param _saleToken Sale token address * @param _treasury Treasury address for proceeds * @param _usdcToken USDC token address * @param _startTime Sale start timestamp @@ -101,18 +106,21 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param _maximumCap Maximum USDC allowed */ constructor( + address _saleToken, address _treasury, - IERC20 _usdcToken, + address _usdcToken, uint256 _startTime, uint256 _endTime, uint256 _minimumCap, uint256 _maximumCap ) Ownable(msg.sender) { + if (_saleToken == address(0)) revert InvalidSaleTokenAddress(_saleToken); if (_treasury == address(0)) revert InvalidTreasuryAddress(_treasury); if (_endTime <= _startTime) revert InvalidEndTime(_startTime, _endTime); + saleToken = IERC20(_saleToken); treasury = _treasury; - USDC = _usdcToken; + USDC = IERC20(_usdcToken); startTime = _startTime; endTime = _endTime; minimumCap = _minimumCap; @@ -168,7 +176,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof(); hasClaimed[msg.sender] = true; - // saleToken.safeTransfer(msg.sender, allocation); // Requires saleToken initialization + saleToken.safeTransfer(msg.sender, allocation); emit Claim(msg.sender, allocation); } From d10c5e61b33ecd9bc262fa0905738ff17868365d Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Fri, 31 Jan 2025 10:03:54 -0600 Subject: [PATCH 09/28] Refactor SealedBidTokenSale contract to enhance error handling with detailed InvalidProof error and update finalize logic; add comprehensive unit tests for contract functionality. --- src/apps/SealedBidTokenSale.sol | 6 +- test/unit/apps/SealedBidTokenSale.sol | 210 ++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 test/unit/apps/SealedBidTokenSale.sol diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 98d1da8e..323ba75a 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -44,7 +44,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /// @notice Tokens have already been claimed error AlreadyClaimed(address user); /// @notice Provided Merkle proof is invalid - error InvalidProof(); + error InvalidProof(bytes32[] proof, bytes32 leaf); /// @notice Merkle root not set for claims error MerkleRootNotSet(); @@ -173,7 +173,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { if (hasClaimed[msg.sender]) revert AlreadyClaimed(msg.sender); bytes32 leaf = keccak256(abi.encodePacked(msg.sender, allocation)); - if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof(); + if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof(proof, leaf); hasClaimed[msg.sender] = true; saleToken.safeTransfer(msg.sender, allocation); @@ -188,7 +188,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { */ function finalize() external onlyOwner { if (finalized) revert AlreadyFinalized(); - if (block.timestamp < endTime && totalDeposited != maximumCap) { + if (block.timestamp < endTime) { revert SaleNotEnded(block.timestamp, endTime); } diff --git a/test/unit/apps/SealedBidTokenSale.sol b/test/unit/apps/SealedBidTokenSale.sol new file mode 100644 index 00000000..1222f551 --- /dev/null +++ b/test/unit/apps/SealedBidTokenSale.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import {Test} from "forge-std/Test.sol"; +import {SealedBidTokenSale} from "@kinto-core/apps/SealedBidTokenSale.sol"; +import {ERC20Mock} from "@kinto-core-test/helpers/ERC20Mock.sol"; +import {MerkleProof} from "@openzeppelin-5.0.1/contracts/utils/cryptography/MerkleProof.sol"; +import {SharedSetup} from "@kinto-core-test/SharedSetup.t.sol"; + +contract SealedBidTokenSaleTest is SharedSetup { + using MerkleProof for bytes32[]; + + SealedBidTokenSale public sale; + ERC20Mock public usdc; + ERC20Mock public saleToken; + + uint256 public startTime; + uint256 public endTime; + uint256 public constant MIN_CAP = 10e6 * 1e6; + uint256 public constant MAX_CAP = 20e6 * 1e6; + + bytes32 public merkleRoot; + bytes32[] public proof; + uint256 public allocation = 500 ether; + + function setUp() public override { + super.setUp(); + + startTime = block.timestamp + 1 days; + endTime = startTime + 4 days; + + // Deploy mock tokens + usdc = new ERC20Mock("USDC", "USDC", 6); + saleToken = new ERC20Mock("K", "KINTO", 18); + + // Deploy sale contract with admin as owner + vm.prank(admin); + sale = new SealedBidTokenSale(address(saleToken), TREASURY, address(usdc), startTime, endTime, MIN_CAP, MAX_CAP); + + // Setup Merkle tree with alice and bob + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = keccak256(abi.encodePacked(alice, allocation)); + leaves[1] = keccak256(abi.encodePacked(bob, allocation * 2)); + merkleRoot = buildRoot(leaves); + proof = getProof(leaves, 0); + } + + // Helper to build Merkle root + function buildRoot(bytes32[] memory leaves) internal pure returns (bytes32) { + bytes32[] memory nodes = leaves; + uint256 n = leaves.length; + while (n > 1) { + for (uint256 i = 0; i < n; i += 2) { + nodes[i / 2] = keccak256(abi.encodePacked(nodes[i], nodes[i + 1])); + } + n = (n + 1) / 2; + } + return nodes[0]; + } + + // Helper to get proof for a leaf + function getProof(bytes32[] memory leaves, uint256 index) internal pure returns (bytes32[] memory) { + bytes32[] memory proof = new bytes32[](leaves.length); + uint256 level = 0; + uint256 n = leaves.length; + + while (n > 1) { + if (index % 2 == 1) { + proof[level] = leaves[index - 1]; + } else if (index + 1 < n) { + proof[level] = leaves[index + 1]; + } + index /= 2; + n = (n + 1) / 2; + level++; + } + return proof; + } + + /* ============ Constructor Tests ============ */ + function testConstructor() public { + assertEq(address(sale.saleToken()), address(saleToken)); + assertEq(address(sale.USDC()), address(usdc)); + assertEq(sale.treasury(), TREASURY); + assertEq(sale.startTime(), startTime); + assertEq(sale.endTime(), endTime); + assertEq(sale.minimumCap(), MIN_CAP); + assertEq(sale.maximumCap(), MAX_CAP); + assertEq(sale.owner(), admin); + } + + /* ============ Deposit Tests ============ */ + function testDeposit() public { + vm.warp(startTime + 1); + uint256 amount = 1000 * 1e6; + + // Mint and approve USDC + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + // Deposit through Kinto Wallet + vm.prank(alice); + sale.deposit(amount); + + assertEq(sale.deposits(alice), amount); + assertEq(sale.totalDeposited(), amount); + assertEq(usdc.balanceOf(address(sale)), amount); + } + + function testDeposit_RevertWhen_BeforeStart() public { + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, block.timestamp, startTime)); + vm.prank(alice); + sale.deposit(100 ether); + } + + /* ============ Finalize Tests ============ */ + function testFinalize_Success() public { + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + + vm.prank(alice); + sale.deposit(MAX_CAP); + + vm.warp(endTime); + vm.prank(admin); + sale.finalize(); + + assertTrue(sale.finalized()); + assertTrue(sale.successful()); + } + + /* ============ Withdraw Tests ============ */ + function testWithdraw() public { + uint256 amount = 1000 * 1e6; + + // Setup failed sale + vm.warp(startTime + 1); + + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + vm.warp(endTime); + vm.prank(admin); + sale.finalize(); + + vm.prank(alice); + sale.withdraw(); + + assertEq(sale.deposits(alice), 0); + assertEq(usdc.balanceOf(alice), amount); + } + + /* ============ Claim Tests ============ */ + function testClaimTokens() public { + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + + vm.prank(alice); + sale.deposit(MAX_CAP); + + vm.warp(endTime); + vm.prank(admin); + sale.finalize(); + + vm.prank(admin); + sale.setMerkleRoot(merkleRoot); + + vm.prank(alice); + sale.claimTokens(allocation, proof); + + assertTrue(sale.hasClaimed(alice)); + assertEq(saleToken.balanceOf(alice), allocation); + } + + /* ============ Admin Function Tests ============ */ + + function testWithdrawProceeds() public { + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + + vm.prank(alice); + sale.deposit(MAX_CAP); + + vm.warp(endTime); + vm.prank(admin); + sale.finalize(); + + vm.prank(admin); + sale.withdrawProceeds(); + + assertEq(usdc.balanceOf(TREASURY), MAX_CAP); + } +} From bd49189153dac486226c4a6680e90acb81ae5be2 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 13:27:06 -0600 Subject: [PATCH 10/28] Refactor SealedBidTokenSale test contract: rename file, update Merkle proof logic, and enhance test coverage. --- ...TokenSale.sol => SealedBidTokenSale.t.sol} | 95 ++++++++++++++----- 1 file changed, 69 insertions(+), 26 deletions(-) rename test/unit/apps/{SealedBidTokenSale.sol => SealedBidTokenSale.t.sol} (66%) diff --git a/test/unit/apps/SealedBidTokenSale.sol b/test/unit/apps/SealedBidTokenSale.t.sol similarity index 66% rename from test/unit/apps/SealedBidTokenSale.sol rename to test/unit/apps/SealedBidTokenSale.t.sol index 1222f551..16254924 100644 --- a/test/unit/apps/SealedBidTokenSale.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -42,43 +42,84 @@ contract SealedBidTokenSaleTest is SharedSetup { leaves[0] = keccak256(abi.encodePacked(alice, allocation)); leaves[1] = keccak256(abi.encodePacked(bob, allocation * 2)); merkleRoot = buildRoot(leaves); - proof = getProof(leaves, 0); + proof = buildProof(leaves, 0); } - // Helper to build Merkle root - function buildRoot(bytes32[] memory leaves) internal pure returns (bytes32) { - bytes32[] memory nodes = leaves; - uint256 n = leaves.length; - while (n > 1) { - for (uint256 i = 0; i < n; i += 2) { - nodes[i / 2] = keccak256(abi.encodePacked(nodes[i], nodes[i + 1])); + // Following code is adapted from https://github.com/dmfxyz/murky/blob/main/src/common/MurkyBase.sol. + function buildRoot(bytes32[] memory leaves) private pure returns (bytes32) { + require(leaves.length > 1); + while (leaves.length > 1) { + leaves = hashLevel(leaves); + } + return leaves[0]; + } + + function buildProof(bytes32[] memory leaves, uint256 nodeIndex) private pure returns (bytes32[] memory) { + require(leaves.length > 1); + + bytes32[] memory result = new bytes32[](64); + uint256 pos; + + while (leaves.length > 1) { + unchecked { + if (nodeIndex & 0x1 == 1) { + result[pos] = leaves[nodeIndex - 1]; + } else if (nodeIndex + 1 == leaves.length) { + result[pos] = bytes32(0); + } else { + result[pos] = leaves[nodeIndex + 1]; + } + ++pos; + nodeIndex /= 2; } - n = (n + 1) / 2; + leaves = hashLevel(leaves); } - return nodes[0]; + // Resize the length of the array to fit. + /// @solidity memory-safe-assembly + assembly { + mstore(result, pos) + } + + return result; } - // Helper to get proof for a leaf - function getProof(bytes32[] memory leaves, uint256 index) internal pure returns (bytes32[] memory) { - bytes32[] memory proof = new bytes32[](leaves.length); - uint256 level = 0; - uint256 n = leaves.length; - - while (n > 1) { - if (index % 2 == 1) { - proof[level] = leaves[index - 1]; - } else if (index + 1 < n) { - proof[level] = leaves[index + 1]; + function hashLevel(bytes32[] memory leaves) private pure returns (bytes32[] memory) { + bytes32[] memory result; + unchecked { + uint256 length = leaves.length; + if (length & 0x1 == 1) { + result = new bytes32[](length / 2 + 1); + result[result.length - 1] = hashPair(leaves[length - 1], bytes32(0)); + } else { + result = new bytes32[](length / 2); + } + uint256 pos = 0; + for (uint256 i = 0; i < length - 1; i += 2) { + result[pos] = hashPair(leaves[i], leaves[i + 1]); + ++pos; + } + } + return result; + } + + function hashPair(bytes32 left, bytes32 right) private pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + switch lt(left, right) + case 0 { + mstore(0x0, right) + mstore(0x20, left) + } + default { + mstore(0x0, left) + mstore(0x20, right) } - index /= 2; - n = (n + 1) / 2; - level++; + result := keccak256(0x0, 0x40) } - return proof; } /* ============ Constructor Tests ============ */ - function testConstructor() public { + function testConstructor() public view { assertEq(address(sale.saleToken()), address(saleToken)); assertEq(address(sale.USDC()), address(usdc)); assertEq(sale.treasury(), TREASURY); @@ -164,6 +205,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.warp(startTime + 1); usdc.mint(alice, MAX_CAP); + saleToken.mint(address(sale), allocation); vm.prank(alice); usdc.approve(address(sale), MAX_CAP); @@ -183,6 +225,7 @@ contract SealedBidTokenSaleTest is SharedSetup { assertTrue(sale.hasClaimed(alice)); assertEq(saleToken.balanceOf(alice), allocation); + assertEq(saleToken.balanceOf(address(sale)), 0); } /* ============ Admin Function Tests ============ */ From f7d46899ed3e1c463ef1d19bf201725d374f5b13 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 13:51:47 -0600 Subject: [PATCH 11/28] Refactor SealedBidTokenSale contract: remove endTime, rename functions and events for clarity, update tests accordingly. --- src/apps/SealedBidTokenSale.sol | 67 ++++++++++--------------- test/unit/apps/SealedBidTokenSale.t.sol | 26 +++++----- 2 files changed, 40 insertions(+), 53 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 323ba75a..8bea021d 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -21,23 +21,17 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error InvalidSaleTokenAddress(address token); /// @notice Invalid treasury address provided (zero address) error InvalidTreasuryAddress(address treasury); - /// @notice End time must be after start time - error InvalidEndTime(uint256 startTime, uint256 endTime); /// @notice Sale period has not started yet error SaleNotStarted(uint256 currentTime, uint256 startTime); /// @notice Sale period has already ended - error SaleEnded(uint256 currentTime, uint256 endTime); - /// @notice Sale has already been finalized - error AlreadyFinalized(); - /// @notice Action requires prior finalization - error NotFinalized(); + error SaleAlreadyEnded(uint256 currentTime); /// @notice Sale period has not ended yet - error SaleNotEnded(uint256 currentTime, uint256 endTime); + error SaleNotEnded(uint256 currentTime); /// @notice Operation requires successful sale state error SaleNotSuccessful(); /// @notice Operation requires failed sale state error SaleWasSuccessful(); - /// @notice Deposit amount must be greater than zero + /// @notice Deposited amount must be greater than zero error ZeroDeposit(); /// @notice No funds available for withdrawal error NothingToWithdraw(address user); @@ -51,15 +45,15 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ Events ============ */ /// @notice Emitted on USDC deposit - event Deposit(address indexed user, uint256 amount); + event Deposited(address indexed user, uint256 amount); /// @notice Emitted on USDC withdrawal - event Withdraw(address indexed user, uint256 amount); - /// @notice Emitted when sale is finalized - event Finalized(bool successful, uint256 totalDeposited); + event Withdrawn(address indexed user, uint256 amount); + /// @notice Emitted when sale is ended + event SaleEnded(bool successful, uint256 totalDeposited); /// @notice Emitted when Merkle root is set event MerkleRootSet(bytes32 root); /// @notice Emitted on successful token claim - event Claim(address indexed user, uint256 tokenAmount); + event Claimed(address indexed user, uint256 tokenAmount); /* ============ Immutable Parameters ============ */ @@ -71,8 +65,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { address public immutable treasury; /// @notice Sale start timestamp uint256 public immutable startTime; - /// @notice Sale end timestamp - uint256 public immutable endTime; /// @notice Minimum USDC required for success uint256 public immutable minimumCap; /// @notice Maximum USDC allowed in sale @@ -81,7 +73,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ State Variables ============ */ /// @notice Sale finalization status - bool public finalized; + bool public saleEnded; /// @notice Sale success status bool public successful; /// @notice Total USDC deposited @@ -101,7 +93,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param _treasury Treasury address for proceeds * @param _usdcToken USDC token address * @param _startTime Sale start timestamp - * @param _endTime Sale end timestamp * @param _minimumCap Minimum USDC for success * @param _maximumCap Maximum USDC allowed */ @@ -116,13 +107,11 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { ) Ownable(msg.sender) { if (_saleToken == address(0)) revert InvalidSaleTokenAddress(_saleToken); if (_treasury == address(0)) revert InvalidTreasuryAddress(_treasury); - if (_endTime <= _startTime) revert InvalidEndTime(_startTime, _endTime); saleToken = IERC20(_saleToken); treasury = _treasury; USDC = IERC20(_usdcToken); startTime = _startTime; - endTime = _endTime; minimumCap = _minimumCap; maximumCap = _maximumCap; } @@ -130,27 +119,26 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ User Functions ============ */ /** - * @notice Deposit USDC into the sale + * @notice Deposited USDC into the sale * @param amount Amount of USDC to deposit */ function deposit(uint256 amount) external nonReentrant { if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); - if (block.timestamp > endTime) revert SaleEnded(block.timestamp, endTime); - if (finalized) revert AlreadyFinalized(); + if (saleEnded) revert SaleAlreadyEnded(block.timestamp); if (amount == 0) revert ZeroDeposit(); USDC.safeTransferFrom(msg.sender, address(this), amount); deposits[msg.sender] += amount; totalDeposited += amount; - emit Deposit(msg.sender, amount); + emit Deposited(msg.sender, amount); } /** - * @notice Withdraw USDC if sale failed + * @notice Withdrawn USDC if sale failed */ function withdraw() external nonReentrant { - if (!finalized) revert NotFinalized(); + if (!saleEnded) revert SaleNotEnded(block.timestamp); if (successful) revert SaleWasSuccessful(); uint256 amount = deposits[msg.sender]; @@ -159,16 +147,16 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { deposits[msg.sender] = 0; USDC.safeTransfer(msg.sender, amount); - emit Withdraw(msg.sender, amount); + emit Withdrawn(msg.sender, amount); } /** - * @notice Claim allocated tokens using Merkle proof + * @notice Claimed allocated tokens using Merkle proof * @param allocation Token amount allocated to sender * @param proof Merkle proof for allocation */ function claimTokens(uint256 allocation, bytes32[] calldata proof) external nonReentrant { - if (!finalized || !successful) revert SaleNotSuccessful(); + if (!saleEnded || !successful) revert SaleNotSuccessful(); if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); if (hasClaimed[msg.sender]) revert AlreadyClaimed(msg.sender); @@ -178,24 +166,21 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { hasClaimed[msg.sender] = true; saleToken.safeTransfer(msg.sender, allocation); - emit Claim(msg.sender, allocation); + emit Claimed(msg.sender, allocation); } /* ============ Admin Functions ============ */ /** - * @notice Finalize sale outcome + * @notice Ends sale */ - function finalize() external onlyOwner { - if (finalized) revert AlreadyFinalized(); - if (block.timestamp < endTime) { - revert SaleNotEnded(block.timestamp, endTime); - } + function endSale() external onlyOwner { + if (saleEnded) revert SaleAlreadyEnded(block.timestamp); - finalized = true; + saleEnded = true; successful = totalDeposited >= minimumCap; - emit Finalized(successful, totalDeposited); + emit SaleEnded(successful, totalDeposited); } /** @@ -203,16 +188,16 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param _merkleRoot Root of allocation Merkle tree */ function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner { - if (!finalized || !successful) revert SaleNotSuccessful(); + if (!saleEnded || !successful) revert SaleNotSuccessful(); merkleRoot = _merkleRoot; emit MerkleRootSet(_merkleRoot); } /** - * @notice Withdraw proceeds to treasury + * @notice Withdrawn proceeds to treasury */ function withdrawProceeds() external onlyOwner { - if (!finalized || !successful) revert SaleNotSuccessful(); + if (!saleEnded || !successful) revert SaleNotSuccessful(); USDC.safeTransfer(treasury, USDC.balanceOf(address(this))); } } diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 16254924..84160009 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -119,12 +119,12 @@ contract SealedBidTokenSaleTest is SharedSetup { } /* ============ Constructor Tests ============ */ + function testConstructor() public view { assertEq(address(sale.saleToken()), address(saleToken)); assertEq(address(sale.USDC()), address(usdc)); assertEq(sale.treasury(), TREASURY); assertEq(sale.startTime(), startTime); - assertEq(sale.endTime(), endTime); assertEq(sale.minimumCap(), MIN_CAP); assertEq(sale.maximumCap(), MAX_CAP); assertEq(sale.owner(), admin); @@ -155,8 +155,9 @@ contract SealedBidTokenSaleTest is SharedSetup { sale.deposit(100 ether); } - /* ============ Finalize Tests ============ */ - function testFinalize_Success() public { + /* ============ endSale ============ */ + + function testEndSale() public { vm.warp(startTime + 1); usdc.mint(alice, MAX_CAP); @@ -167,15 +168,15 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); sale.deposit(MAX_CAP); - vm.warp(endTime); vm.prank(admin); - sale.finalize(); + sale.endSale(); - assertTrue(sale.finalized()); + assertTrue(sale.saleEnded()); assertTrue(sale.successful()); } - /* ============ Withdraw Tests ============ */ + /* ============ Withdraw ============ */ + function testWithdraw() public { uint256 amount = 1000 * 1e6; @@ -191,7 +192,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.warp(endTime); vm.prank(admin); - sale.finalize(); + sale.endSale(); vm.prank(alice); sale.withdraw(); @@ -200,7 +201,8 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(usdc.balanceOf(alice), amount); } - /* ============ Claim Tests ============ */ + /* ============ claimTokens ============ */ + function testClaimTokens() public { vm.warp(startTime + 1); @@ -215,7 +217,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.warp(endTime); vm.prank(admin); - sale.finalize(); + sale.endSale(); vm.prank(admin); sale.setMerkleRoot(merkleRoot); @@ -228,7 +230,7 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(saleToken.balanceOf(address(sale)), 0); } - /* ============ Admin Function Tests ============ */ + /* ============ withdrawProceeds ============ */ function testWithdrawProceeds() public { vm.warp(startTime + 1); @@ -243,7 +245,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.warp(endTime); vm.prank(admin); - sale.finalize(); + sale.endSale(); vm.prank(admin); sale.withdrawProceeds(); From 9acb3dfa24349dedecc698dfeb58cb5782da11a1 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 13:57:06 -0600 Subject: [PATCH 12/28] Remove maximumCap from SealedBidTokenSale contract and update related tests and constructor parameters. --- src/apps/SealedBidTokenSale.sol | 24 +++++++----------------- test/unit/apps/SealedBidTokenSale.t.sol | 5 ++--- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 8bea021d..31a14744 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -67,8 +67,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { uint256 public immutable startTime; /// @notice Minimum USDC required for success uint256 public immutable minimumCap; - /// @notice Maximum USDC allowed in sale - uint256 public immutable maximumCap; /* ============ State Variables ============ */ @@ -94,17 +92,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param _usdcToken USDC token address * @param _startTime Sale start timestamp * @param _minimumCap Minimum USDC for success - * @param _maximumCap Maximum USDC allowed */ - constructor( - address _saleToken, - address _treasury, - address _usdcToken, - uint256 _startTime, - uint256 _endTime, - uint256 _minimumCap, - uint256 _maximumCap - ) Ownable(msg.sender) { + constructor(address _saleToken, address _treasury, address _usdcToken, uint256 _startTime, uint256 _minimumCap) + Ownable(msg.sender) + { if (_saleToken == address(0)) revert InvalidSaleTokenAddress(_saleToken); if (_treasury == address(0)) revert InvalidTreasuryAddress(_treasury); @@ -113,7 +104,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { USDC = IERC20(_usdcToken); startTime = _startTime; minimumCap = _minimumCap; - maximumCap = _maximumCap; } /* ============ User Functions ============ */ @@ -185,12 +175,12 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /** * @notice Set Merkle root for allocations - * @param _merkleRoot Root of allocation Merkle tree + * @param newRoot Root of allocation Merkle tree */ - function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner { + function setMerkleRoot(bytes32 newRoot) external onlyOwner { if (!saleEnded || !successful) revert SaleNotSuccessful(); - merkleRoot = _merkleRoot; - emit MerkleRootSet(_merkleRoot); + merkleRoot = newRoot; + emit MerkleRootSet(newRoot); } /** diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 84160009..3faaaf01 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -21,7 +21,7 @@ contract SealedBidTokenSaleTest is SharedSetup { bytes32 public merkleRoot; bytes32[] public proof; - uint256 public allocation = 500 ether; + uint256 public allocation = 1000 * 1e18; function setUp() public override { super.setUp(); @@ -35,7 +35,7 @@ contract SealedBidTokenSaleTest is SharedSetup { // Deploy sale contract with admin as owner vm.prank(admin); - sale = new SealedBidTokenSale(address(saleToken), TREASURY, address(usdc), startTime, endTime, MIN_CAP, MAX_CAP); + sale = new SealedBidTokenSale(address(saleToken), TREASURY, address(usdc), startTime, MIN_CAP); // Setup Merkle tree with alice and bob bytes32[] memory leaves = new bytes32[](2); @@ -126,7 +126,6 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(sale.treasury(), TREASURY); assertEq(sale.startTime(), startTime); assertEq(sale.minimumCap(), MIN_CAP); - assertEq(sale.maximumCap(), MAX_CAP); assertEq(sale.owner(), admin); } From bb7e1101c74f4b03d9857592e92b58cec8192c7e Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 14:15:24 -0600 Subject: [PATCH 13/28] Refactor SealedBidTokenSale contract and test to include USDC allocation in claimTokens function, update Merkle proof logic, and adjust test setup and assertions accordingly. --- src/apps/SealedBidTokenSale.sol | 20 +++++++++++++++----- test/unit/apps/SealedBidTokenSale.t.sol | 15 +++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 31a14744..09c70b96 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -142,21 +142,31 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /** * @notice Claimed allocated tokens using Merkle proof - * @param allocation Token amount allocated to sender + * @param saleTokenAllocation Token amount allocated to sender * @param proof Merkle proof for allocation */ - function claimTokens(uint256 allocation, bytes32[] calldata proof) external nonReentrant { + function claimTokens(uint256 saleTokenAllocation, uint256 usdcAllocation, bytes32[] calldata proof) + external + nonReentrant + { if (!saleEnded || !successful) revert SaleNotSuccessful(); if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); if (hasClaimed[msg.sender]) revert AlreadyClaimed(msg.sender); - bytes32 leaf = keccak256(abi.encodePacked(msg.sender, allocation)); + bytes32 leaf = keccak256(abi.encodePacked(msg.sender, saleTokenAllocation, usdcAllocation)); if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof(proof, leaf); hasClaimed[msg.sender] = true; - saleToken.safeTransfer(msg.sender, allocation); - emit Claimed(msg.sender, allocation); + if (saleTokenAllocation > 0) { + saleToken.safeTransfer(msg.sender, saleTokenAllocation); + } + + if (usdcAllocation > 0) { + USDC.safeTransfer(msg.sender, usdcAllocation); + } + + emit Claimed(msg.sender, saleTokenAllocation); } /* ============ Admin Functions ============ */ diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 3faaaf01..78e36eb6 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -21,7 +21,8 @@ contract SealedBidTokenSaleTest is SharedSetup { bytes32 public merkleRoot; bytes32[] public proof; - uint256 public allocation = 1000 * 1e18; + uint256 public saleTokenAllocation = 1000 * 1e18; + uint256 public usdcAllocation = 1000 * 1e6; function setUp() public override { super.setUp(); @@ -39,8 +40,9 @@ contract SealedBidTokenSaleTest is SharedSetup { // Setup Merkle tree with alice and bob bytes32[] memory leaves = new bytes32[](2); - leaves[0] = keccak256(abi.encodePacked(alice, allocation)); - leaves[1] = keccak256(abi.encodePacked(bob, allocation * 2)); + leaves[0] = keccak256(abi.encodePacked(alice, saleTokenAllocation, usdcAllocation)); + leaves[1] = keccak256(abi.encodePacked(bob, saleTokenAllocation * 2, usdcAllocation)); + merkleRoot = buildRoot(leaves); proof = buildProof(leaves, 0); } @@ -206,7 +208,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.warp(startTime + 1); usdc.mint(alice, MAX_CAP); - saleToken.mint(address(sale), allocation); + saleToken.mint(address(sale), saleTokenAllocation); vm.prank(alice); usdc.approve(address(sale), MAX_CAP); @@ -222,10 +224,11 @@ contract SealedBidTokenSaleTest is SharedSetup { sale.setMerkleRoot(merkleRoot); vm.prank(alice); - sale.claimTokens(allocation, proof); + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof); assertTrue(sale.hasClaimed(alice)); - assertEq(saleToken.balanceOf(alice), allocation); + assertEq(saleToken.balanceOf(alice), saleTokenAllocation); + assertEq(usdc.balanceOf(alice), usdcAllocation); assertEq(saleToken.balanceOf(address(sale)), 0); } From 2621d280331f2f23ce0c95aaa5fb5425ac36653e Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 14:21:55 -0600 Subject: [PATCH 14/28] Refactor BridgerL2.t.sol by moving 'deal' function call for gasFee after setting bridgeData.gasFee. --- test/fork/bridger/BridgerL2.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fork/bridger/BridgerL2.t.sol b/test/fork/bridger/BridgerL2.t.sol index 69aa8551..0b4e1831 100644 --- a/test/fork/bridger/BridgerL2.t.sol +++ b/test/fork/bridger/BridgerL2.t.sol @@ -49,7 +49,6 @@ contract BridgerL2Test is SignatureHelper, SharedSetup, BridgeDataHelper { uint256 fee = 1; IBridger.BridgeData memory bridgeData = bridgeData[block.chainid][DAI_KINTO]; - deal(address(_bridgerL2), bridgeData.gasFee); deal(inputAsset, address(_kintoWallet), amountIn + fee); @@ -60,6 +59,7 @@ contract BridgerL2Test is SignatureHelper, SharedSetup, BridgeDataHelper { IERC20(inputAsset).approve(address(_bridgerL2), amountIn + fee); bridgeData.gasFee = IBridge(bridgeData.vault).getMinFees(bridgeData.connector, bridgeData.msgGasLimit, 322); + deal(address(_bridgerL2), bridgeData.gasFee); vm.startPrank(address(_kintoWallet)); _bridgerL2.withdrawERC20(inputAsset, amountIn, _kintoWallet.owners(0), fee, bridgeData); From 18ba6f61337d203024e03e11f9ebe09b26baafd7 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 14:26:48 -0600 Subject: [PATCH 15/28] Refactor SealedBidTokenSale contract to replace 'successful' with 'capReached' for clarity in sale status logic. --- src/apps/SealedBidTokenSale.sol | 20 ++++++++++---------- test/unit/apps/SealedBidTokenSale.t.sol | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 09c70b96..007833e9 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -27,7 +27,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error SaleAlreadyEnded(uint256 currentTime); /// @notice Sale period has not ended yet error SaleNotEnded(uint256 currentTime); - /// @notice Operation requires successful sale state + /// @notice Operation requires capReached sale state error SaleNotSuccessful(); /// @notice Operation requires failed sale state error SaleWasSuccessful(); @@ -49,10 +49,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /// @notice Emitted on USDC withdrawal event Withdrawn(address indexed user, uint256 amount); /// @notice Emitted when sale is ended - event SaleEnded(bool successful, uint256 totalDeposited); + event SaleEnded(bool capReached, uint256 totalDeposited); /// @notice Emitted when Merkle root is set event MerkleRootSet(bytes32 root); - /// @notice Emitted on successful token claim + /// @notice Emitted on capReached token claim event Claimed(address indexed user, uint256 tokenAmount); /* ============ Immutable Parameters ============ */ @@ -73,7 +73,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /// @notice Sale finalization status bool public saleEnded; /// @notice Sale success status - bool public successful; + bool public capReached; /// @notice Total USDC deposited uint256 public totalDeposited; /// @notice Merkle root for allocations @@ -129,7 +129,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { */ function withdraw() external nonReentrant { if (!saleEnded) revert SaleNotEnded(block.timestamp); - if (successful) revert SaleWasSuccessful(); + if (capReached) revert SaleWasSuccessful(); uint256 amount = deposits[msg.sender]; if (amount == 0) revert NothingToWithdraw(msg.sender); @@ -149,7 +149,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { external nonReentrant { - if (!saleEnded || !successful) revert SaleNotSuccessful(); + if (!saleEnded || !capReached) revert SaleNotSuccessful(); if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); if (hasClaimed[msg.sender]) revert AlreadyClaimed(msg.sender); @@ -178,9 +178,9 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { if (saleEnded) revert SaleAlreadyEnded(block.timestamp); saleEnded = true; - successful = totalDeposited >= minimumCap; + capReached = totalDeposited >= minimumCap; - emit SaleEnded(successful, totalDeposited); + emit SaleEnded(capReached, totalDeposited); } /** @@ -188,7 +188,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param newRoot Root of allocation Merkle tree */ function setMerkleRoot(bytes32 newRoot) external onlyOwner { - if (!saleEnded || !successful) revert SaleNotSuccessful(); + if (!saleEnded || !capReached) revert SaleNotSuccessful(); merkleRoot = newRoot; emit MerkleRootSet(newRoot); } @@ -197,7 +197,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @notice Withdrawn proceeds to treasury */ function withdrawProceeds() external onlyOwner { - if (!saleEnded || !successful) revert SaleNotSuccessful(); + if (!saleEnded || !capReached) revert SaleNotSuccessful(); USDC.safeTransfer(treasury, USDC.balanceOf(address(this))); } } diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 78e36eb6..32798235 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -173,7 +173,7 @@ contract SealedBidTokenSaleTest is SharedSetup { sale.endSale(); assertTrue(sale.saleEnded()); - assertTrue(sale.successful()); + assertTrue(sale.capReached()); } /* ============ Withdraw ============ */ From 5e239a7d158de5ca06c65fdc057b4b0e7fd2dd5b Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 17:04:29 -0600 Subject: [PATCH 16/28] Refactor SealedBidTokenSale contract to include user address in claimTokens function and update Merkle proof handling in tests. --- src/apps/SealedBidTokenSale.sol | 19 +++++++++++-------- test/unit/apps/SealedBidTokenSale.t.sol | 7 +++---- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 007833e9..3d98e951 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -90,6 +90,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param _saleToken Sale token address * @param _treasury Treasury address for proceeds * @param _usdcToken USDC token address + * * @param _startTime Sale start timestamp * @param _minimumCap Minimum USDC for success */ @@ -117,10 +118,11 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { if (saleEnded) revert SaleAlreadyEnded(block.timestamp); if (amount == 0) revert ZeroDeposit(); - USDC.safeTransferFrom(msg.sender, address(this), amount); deposits[msg.sender] += amount; totalDeposited += amount; + USDC.safeTransferFrom(msg.sender, address(this), amount); + emit Deposited(msg.sender, amount); } @@ -144,29 +146,30 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @notice Claimed allocated tokens using Merkle proof * @param saleTokenAllocation Token amount allocated to sender * @param proof Merkle proof for allocation + * @param user User to claim tokens for */ - function claimTokens(uint256 saleTokenAllocation, uint256 usdcAllocation, bytes32[] calldata proof) + function claimTokens(uint256 saleTokenAllocation, uint256 usdcAllocation, bytes32[] calldata proof, address user) external nonReentrant { if (!saleEnded || !capReached) revert SaleNotSuccessful(); if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); - if (hasClaimed[msg.sender]) revert AlreadyClaimed(msg.sender); + if (hasClaimed[user]) revert AlreadyClaimed(user); - bytes32 leaf = keccak256(abi.encodePacked(msg.sender, saleTokenAllocation, usdcAllocation)); + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(user, saleTokenAllocation, usdcAllocation)))); if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof(proof, leaf); - hasClaimed[msg.sender] = true; + hasClaimed[user] = true; if (saleTokenAllocation > 0) { - saleToken.safeTransfer(msg.sender, saleTokenAllocation); + saleToken.safeTransfer(user, saleTokenAllocation); } if (usdcAllocation > 0) { - USDC.safeTransfer(msg.sender, usdcAllocation); + USDC.safeTransfer(user, usdcAllocation); } - emit Claimed(msg.sender, saleTokenAllocation); + emit Claimed(user, saleTokenAllocation); } /* ============ Admin Functions ============ */ diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 32798235..53a6633c 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -40,8 +40,8 @@ contract SealedBidTokenSaleTest is SharedSetup { // Setup Merkle tree with alice and bob bytes32[] memory leaves = new bytes32[](2); - leaves[0] = keccak256(abi.encodePacked(alice, saleTokenAllocation, usdcAllocation)); - leaves[1] = keccak256(abi.encodePacked(bob, saleTokenAllocation * 2, usdcAllocation)); + leaves[0] = keccak256(bytes.concat(keccak256(abi.encode(alice, saleTokenAllocation, usdcAllocation)))); + leaves[1] = keccak256(bytes.concat(keccak256(abi.encode(bob, saleTokenAllocation * 2, usdcAllocation)))); merkleRoot = buildRoot(leaves); proof = buildProof(leaves, 0); @@ -223,8 +223,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(admin); sale.setMerkleRoot(merkleRoot); - vm.prank(alice); - sale.claimTokens(saleTokenAllocation, usdcAllocation, proof); + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof, alice); assertTrue(sale.hasClaimed(alice)); assertEq(saleToken.balanceOf(alice), saleTokenAllocation); From f3f1a4cefbacb1660aa2b7436f35d289fa69c90a Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 17:43:47 -0600 Subject: [PATCH 17/28] Refactor error handling in SealedBidTokenSale contract and enhance unit tests with additional scenarios for deposit, endSale, withdraw, claimTokens, setMerkleRoot, and withdrawProceeds functions. --- src/apps/SealedBidTokenSale.sol | 8 +- test/unit/apps/SealedBidTokenSale.t.sol | 797 +++++++++++++++++++++++- 2 files changed, 794 insertions(+), 11 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 3d98e951..f62dd141 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -28,7 +28,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /// @notice Sale period has not ended yet error SaleNotEnded(uint256 currentTime); /// @notice Operation requires capReached sale state - error SaleNotSuccessful(); + error CapNotReached(); /// @notice Operation requires failed sale state error SaleWasSuccessful(); /// @notice Deposited amount must be greater than zero @@ -152,7 +152,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { external nonReentrant { - if (!saleEnded || !capReached) revert SaleNotSuccessful(); + if (!saleEnded || !capReached) revert CapNotReached(); if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); if (hasClaimed[user]) revert AlreadyClaimed(user); @@ -191,7 +191,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param newRoot Root of allocation Merkle tree */ function setMerkleRoot(bytes32 newRoot) external onlyOwner { - if (!saleEnded || !capReached) revert SaleNotSuccessful(); + if (!saleEnded || !capReached) revert CapNotReached(); merkleRoot = newRoot; emit MerkleRootSet(newRoot); } @@ -200,7 +200,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @notice Withdrawn proceeds to treasury */ function withdrawProceeds() external onlyOwner { - if (!saleEnded || !capReached) revert SaleNotSuccessful(); + if (!saleEnded || !capReached) revert CapNotReached(); USDC.safeTransfer(treasury, USDC.balanceOf(address(this))); } } diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 53a6633c..453dd3bd 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; +import {Ownable} from "@openzeppelin-5.0.1/contracts/access/Ownable.sol"; import {Test} from "forge-std/Test.sol"; import {SealedBidTokenSale} from "@kinto-core/apps/SealedBidTokenSale.sol"; import {ERC20Mock} from "@kinto-core-test/helpers/ERC20Mock.sol"; @@ -49,7 +50,7 @@ contract SealedBidTokenSaleTest is SharedSetup { // Following code is adapted from https://github.com/dmfxyz/murky/blob/main/src/common/MurkyBase.sol. function buildRoot(bytes32[] memory leaves) private pure returns (bytes32) { - require(leaves.length > 1); + require(leaves.length > 1, "leaves.length > 1"); while (leaves.length > 1) { leaves = hashLevel(leaves); } @@ -57,7 +58,7 @@ contract SealedBidTokenSaleTest is SharedSetup { } function buildProof(bytes32[] memory leaves, uint256 nodeIndex) private pure returns (bytes32[] memory) { - require(leaves.length > 1); + require(leaves.length > 1, "leaves.length > 1"); bytes32[] memory result = new bytes32[](64); uint256 pos; @@ -120,7 +121,7 @@ contract SealedBidTokenSaleTest is SharedSetup { } } - /* ============ Constructor Tests ============ */ + /* ============ Constructor ============ */ function testConstructor() public view { assertEq(address(sale.saleToken()), address(saleToken)); @@ -131,7 +132,8 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(sale.owner(), admin); } - /* ============ Deposit Tests ============ */ + /* ============ Deposit ============ */ + function testDeposit() public { vm.warp(startTime + 1); uint256 amount = 1000 * 1e6; @@ -156,6 +158,94 @@ contract SealedBidTokenSaleTest is SharedSetup { sale.deposit(100 ether); } + function testDeposit_RevertWhen_SaleEnded() public { + // Advance time to start of sale + vm.warp(startTime + 1); + + // End the sale + vm.prank(admin); + sale.endSale(); + + // Try to deposit after sale has ended + uint256 amount = 1000 * 1e6; + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleAlreadyEnded.selector, block.timestamp)); + sale.deposit(amount); + } + + function testDeposit_RevertWhen_ZeroAmount() public { + // Advance time to start of sale + vm.warp(startTime + 1); + + // Try to deposit zero amount + vm.prank(alice); + vm.expectRevert(SealedBidTokenSale.ZeroDeposit.selector); + sale.deposit(0); + } + + function testDeposit_MultipleDeposits() public { + // Advance time to start of sale + vm.warp(startTime + 1); + + uint256 firstAmount = 1000 * 1e6; + uint256 secondAmount = 2000 * 1e6; + uint256 totalAmount = firstAmount + secondAmount; + + // Mint and approve USDC for both deposits + usdc.mint(alice, totalAmount); + vm.prank(alice); + usdc.approve(address(sale), totalAmount); + + // Make first deposit + vm.prank(alice); + sale.deposit(firstAmount); + + // Make second deposit + vm.prank(alice); + sale.deposit(secondAmount); + + // Verify final state + assertEq(sale.deposits(alice), totalAmount); + assertEq(sale.totalDeposited(), totalAmount); + assertEq(usdc.balanceOf(address(sale)), totalAmount); + } + + function testDeposit_MultipleUsers() public { + // Advance time to start of sale + vm.warp(startTime + 1); + + uint256 aliceAmount = 1000 * 1e6; + uint256 bobAmount = 2000 * 1e6; + uint256 totalAmount = aliceAmount + bobAmount; + + // Setup Alice's deposit + usdc.mint(alice, aliceAmount); + vm.prank(alice); + usdc.approve(address(sale), aliceAmount); + + // Setup Bob's deposit + usdc.mint(bob, bobAmount); + vm.prank(bob); + usdc.approve(address(sale), bobAmount); + + // Make deposits + vm.prank(alice); + sale.deposit(aliceAmount); + + vm.prank(bob); + sale.deposit(bobAmount); + + // Verify final state + assertEq(sale.deposits(alice), aliceAmount); + assertEq(sale.deposits(bob), bobAmount); + assertEq(sale.totalDeposited(), totalAmount); + assertEq(usdc.balanceOf(address(sale)), totalAmount); + } + /* ============ endSale ============ */ function testEndSale() public { @@ -176,6 +266,113 @@ contract SealedBidTokenSaleTest is SharedSetup { assertTrue(sale.capReached()); } + function testEndSale_WhenCapNotReached() public { + // Setup sale with amount below minimum cap + vm.warp(startTime + 1); + uint256 amount = MIN_CAP - 1e6; // Just under minimum cap + + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + // End sale + vm.prank(admin); + sale.endSale(); + + assertTrue(sale.saleEnded()); + assertFalse(sale.capReached()); + assertEq(sale.totalDeposited(), amount); + } + + function testEndSale_ExactlyAtMinCap() public { + // Setup sale with amount exactly at minimum cap + vm.warp(startTime + 1); + uint256 amount = MIN_CAP; + + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + // End sale + vm.prank(admin); + sale.endSale(); + + assertTrue(sale.saleEnded()); + assertTrue(sale.capReached()); + assertEq(sale.totalDeposited(), amount); + } + + function testEndSale_RevertWhen_NotOwner() public { + vm.warp(startTime + 1); + + vm.prank(alice); // Non-owner tries to end sale + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); + sale.endSale(); + } + + function testEndSale_RevertWhen_AlreadyEnded() public { + vm.warp(startTime + 1); + + // First end sale + vm.prank(admin); + sale.endSale(); + + // Try to end sale again + vm.prank(admin); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleAlreadyEnded.selector, block.timestamp)); + sale.endSale(); + } + + function testEndSale_WithNoDeposits() public { + vm.warp(startTime + 1); + + // End sale with no deposits + vm.prank(admin); + sale.endSale(); + + assertTrue(sale.saleEnded()); + assertFalse(sale.capReached()); + assertEq(sale.totalDeposited(), 0); + } + + function testEndSale_WithMultipleDeposits() public { + vm.warp(startTime + 1); + + // Setup multiple deposits that sum to above minimum cap + uint256 aliceAmount = MIN_CAP / 2; + uint256 bobAmount = MIN_CAP / 2 + 1e6; // Slightly more to go over MIN_CAP + + // Alice's deposit + usdc.mint(alice, aliceAmount); + vm.prank(alice); + usdc.approve(address(sale), aliceAmount); + + vm.prank(alice); + sale.deposit(aliceAmount); + + // Bob's deposit + usdc.mint(bob, bobAmount); + vm.prank(bob); + usdc.approve(address(sale), bobAmount); + + vm.prank(bob); + sale.deposit(bobAmount); + + // End sale + vm.prank(admin); + sale.endSale(); + + assertTrue(sale.saleEnded()); + assertTrue(sale.capReached()); + assertEq(sale.totalDeposited(), aliceAmount + bobAmount); + } + /* ============ Withdraw ============ */ function testWithdraw() public { @@ -202,6 +399,131 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(usdc.balanceOf(alice), amount); } + function testWithdraw_RevertWhen_SaleNotEnded() public { + // Setup deposit + vm.warp(startTime + 1); + + uint256 amount = 1000 * 1e6; + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + // Attempt withdrawal before sale ends + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleNotEnded.selector, block.timestamp)); + sale.withdraw(); + } + + function testWithdraw_RevertWhen_CapReached() public { + // Setup successful sale + vm.warp(startTime + 1); + + uint256 amount = MIN_CAP; // Use minimum cap to ensure success + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + // End sale successfully + vm.prank(admin); + sale.endSale(); + + // Attempt withdrawal on successful sale + vm.prank(alice); + vm.expectRevert(SealedBidTokenSale.SaleWasSuccessful.selector); + sale.withdraw(); + } + + function testWithdraw_RevertWhen_NoDeposit() public { + vm.warp(startTime + 1); + + vm.prank(admin); + sale.endSale(); + + // Attempt withdrawal with no deposit + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.NothingToWithdraw.selector, alice)); + sale.withdraw(); + } + + function testWithdraw_MultipleUsers() public { + // Setup deposits for multiple users + vm.warp(startTime + 1); + uint256 aliceAmount = 1000 * 1e6; + uint256 bobAmount = 2000 * 1e6; + + // Setup and execute Alice's deposit + usdc.mint(alice, aliceAmount); + vm.prank(alice); + usdc.approve(address(sale), aliceAmount); + + vm.prank(alice); + sale.deposit(aliceAmount); + + // Setup and execute Bob's deposit + usdc.mint(bob, bobAmount); + vm.prank(bob); + usdc.approve(address(sale), bobAmount); + + vm.prank(bob); + sale.deposit(bobAmount); + + // End sale as failed + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + // Execute withdrawals + vm.prank(alice); + sale.withdraw(); + + vm.prank(bob); + sale.withdraw(); + + // Verify final states + assertEq(sale.deposits(alice), 0); + assertEq(sale.deposits(bob), 0); + assertEq(usdc.balanceOf(alice), aliceAmount); + assertEq(usdc.balanceOf(bob), bobAmount); + assertEq(usdc.balanceOf(address(sale)), 0); + } + + function testWithdraw_RevertWhen_DoubleWithdraw() public { + // Setup deposit + vm.warp(startTime + 1); + + uint256 amount = 1000 * 1e6; + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + // End sale as failed + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + // First withdrawal (should succeed) + vm.prank(alice); + sale.withdraw(); + + // Second withdrawal attempt (should fail) + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.NothingToWithdraw.selector, alice)); + sale.withdraw(); + + // Verify final state + assertEq(sale.deposits(alice), 0); + assertEq(usdc.balanceOf(alice), amount); + } + /* ============ claimTokens ============ */ function testClaimTokens() public { @@ -231,12 +553,60 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(saleToken.balanceOf(address(sale)), 0); } - /* ============ withdrawProceeds ============ */ + function testClaimTokens_RevertWhen_SaleNotEnded() public { + // Sale not ended yet + vm.warp(startTime + 1); - function testWithdrawProceeds() public { + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.CapNotReached.selector)); + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof, alice); + } + + function testClaimTokens_RevertWhen_SaleNotSuccessful() public { + // Setup failed sale (deposit less than min cap) + vm.warp(startTime + 1); + uint256 amount = MIN_CAP - 1e6; // Just under minimum cap + + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + // End sale (will fail due to not meeting min cap) + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.CapNotReached.selector)); + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof, alice); + } + + function testClaimTokens_RevertWhen_MerkleRootNotSet() public { + // Setup successful sale vm.warp(startTime + 1); usdc.mint(alice, MAX_CAP); + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + + vm.prank(alice); + sale.deposit(MAX_CAP); + + vm.prank(admin); + sale.endSale(); + + // Attempt claim before merkle root is set + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.MerkleRootNotSet.selector)); + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof, alice); + } + + function testClaimTokens_RevertWhen_InvalidProof() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + saleToken.mint(address(sale), saleTokenAllocation); vm.prank(alice); usdc.approve(address(sale), MAX_CAP); @@ -248,9 +618,422 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(admin); sale.endSale(); + vm.prank(admin); + sale.setMerkleRoot(merkleRoot); + + // Create invalid proof by using bob's proof for alice + bytes32[] memory invalidProof = buildProof( + new bytes32[](2), // Empty leaves will create invalid proof + 0 + ); + + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(alice, saleTokenAllocation, usdcAllocation)))); + + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.InvalidProof.selector, invalidProof, leaf)); + sale.claimTokens(saleTokenAllocation, usdcAllocation, invalidProof, alice); + } + + function testClaimTokens_RevertWhen_AlreadyClaimed() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + saleToken.mint(address(sale), saleTokenAllocation); + + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + + vm.prank(alice); + sale.deposit(MAX_CAP); + + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + vm.prank(admin); + sale.setMerkleRoot(merkleRoot); + + // First claim (should succeed) + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof, alice); + + // Second claim attempt (should fail) + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.AlreadyClaimed.selector, alice)); + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof, alice); + } + + function testClaimTokens_WhenZeroTokenAllocation() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + usdc.mint(address(sale), usdcAllocation); + + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + + vm.prank(alice); + sale.deposit(MAX_CAP); + + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + // Create merkle tree with zero token allocation + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = keccak256(bytes.concat(keccak256(abi.encode(alice, 0, usdcAllocation)))); + leaves[1] = keccak256(bytes.concat(keccak256(abi.encode(bob, 0, 0)))); + + bytes32 newRoot = buildRoot(leaves); + bytes32[] memory zeroTokenProof = buildProof(leaves, 0); + + vm.prank(admin); + sale.setMerkleRoot(newRoot); + + // Claim with zero token allocation + uint256 initialBalance = saleToken.balanceOf(alice); + sale.claimTokens(0, usdcAllocation, zeroTokenProof, alice); + + // Verify only USDC was transferred + assertEq(saleToken.balanceOf(alice), initialBalance); + assertEq(usdc.balanceOf(alice), usdcAllocation); + assertTrue(sale.hasClaimed(alice)); + } + + function testClaimTokens_WhenZeroUSDCAllocation() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + saleToken.mint(address(sale), saleTokenAllocation); + + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + vm.prank(alice); + sale.deposit(MAX_CAP); + + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + // Create merkle tree with zero USDC allocation + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = keccak256(bytes.concat(keccak256(abi.encode(alice, saleTokenAllocation, 0)))); + leaves[1] = keccak256(bytes.concat(keccak256(abi.encode(bob, 0, 0)))); + + bytes32 newRoot = buildRoot(leaves); + bytes32[] memory zeroUsdcProof = buildProof(leaves, 0); + + vm.prank(admin); + sale.setMerkleRoot(newRoot); + + // Claim with zero USDC allocation + uint256 initialUsdcBalance = usdc.balanceOf(alice); + sale.claimTokens(saleTokenAllocation, 0, zeroUsdcProof, alice); + + // Verify only tokens were transferred + assertEq(saleToken.balanceOf(alice), saleTokenAllocation); + assertEq(usdc.balanceOf(alice), initialUsdcBalance); + assertTrue(sale.hasClaimed(alice)); + } + + /* ============ setMerkleRoot ============ */ + + function testSetMerkleRoot() public { + // Setup successful sale first + vm.warp(startTime + 1); + + usdc.mint(alice, MIN_CAP); + vm.prank(alice); + usdc.approve(address(sale), MIN_CAP); + + vm.prank(alice); + sale.deposit(MIN_CAP); + + vm.prank(admin); + sale.endSale(); + + // Set merkle root + bytes32 newRoot = keccak256("newRoot"); + vm.prank(admin); + vm.expectEmit(false, false, false, true); + emit SealedBidTokenSale.MerkleRootSet(newRoot); + sale.setMerkleRoot(newRoot); + + assertEq(sale.merkleRoot(), newRoot); + } + + function testSetMerkleRoot_RevertWhen_SaleNotEnded() public { + // Try to set merkle root before sale ends + vm.warp(startTime + 1); + + bytes32 newRoot = keccak256("newRoot"); + vm.prank(admin); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.CapNotReached.selector)); + sale.setMerkleRoot(newRoot); + } + + function testSetMerkleRoot_RevertWhen_CapNotReached() public { + // Setup failed sale (below MIN_CAP) + vm.warp(startTime + 1); + + uint256 amount = MIN_CAP - 1e6; // Just below minimum cap + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + vm.prank(admin); + sale.endSale(); + + // Try to set merkle root + bytes32 newRoot = keccak256("newRoot"); + vm.prank(admin); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.CapNotReached.selector)); + sale.setMerkleRoot(newRoot); + } + + function testSetMerkleRoot_RevertWhen_NotOwner() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MIN_CAP); + vm.prank(alice); + usdc.approve(address(sale), MIN_CAP); + vm.prank(alice); + sale.deposit(MIN_CAP); + + vm.prank(admin); + sale.endSale(); + + // Try to set merkle root from non-owner + bytes32 newRoot = keccak256("newRoot"); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); + sale.setMerkleRoot(newRoot); + } + + function testSetMerkleRoot_UpdateExisting() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MIN_CAP); + vm.prank(alice); + usdc.approve(address(sale), MIN_CAP); + + vm.prank(alice); + sale.deposit(MIN_CAP); + + vm.prank(admin); + sale.endSale(); + + // Set initial root + bytes32 initialRoot = keccak256("initialRoot"); + vm.prank(admin); + sale.setMerkleRoot(initialRoot); + assertEq(sale.merkleRoot(), initialRoot); + + // Update to new root + bytes32 newRoot = keccak256("newRoot"); + vm.prank(admin); + sale.setMerkleRoot(newRoot); + assertEq(sale.merkleRoot(), newRoot); + } + + function testSetMerkleRoot_ZeroRoot() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MIN_CAP); + vm.prank(alice); + usdc.approve(address(sale), MIN_CAP); + vm.prank(alice); + sale.deposit(MIN_CAP); + + vm.prank(admin); + sale.endSale(); + + // Set zero merkle root + bytes32 zeroRoot = bytes32(0); + vm.prank(admin); + sale.setMerkleRoot(zeroRoot); + assertEq(sale.merkleRoot(), zeroRoot); + } + + /* ============ withdrawProceeds ============ */ + + function testWithdrawProceeds() public { + // Setup successful sale + vm.warp(startTime + 1); + + uint256 depositAmount = MIN_CAP; + usdc.mint(alice, depositAmount); + vm.prank(alice); + usdc.approve(address(sale), depositAmount); + + vm.prank(alice); + sale.deposit(depositAmount); + + vm.prank(admin); + sale.endSale(); + + // Check initial balances + uint256 initialTreasuryBalance = usdc.balanceOf(TREASURY); + uint256 initialSaleBalance = usdc.balanceOf(address(sale)); + + // Withdraw proceeds + vm.prank(admin); + sale.withdrawProceeds(); + + // Verify balances after withdrawal + assertEq(usdc.balanceOf(TREASURY), initialTreasuryBalance + depositAmount); + assertEq(usdc.balanceOf(address(sale)), 0); + } + + function testWithdrawProceeds_RevertWhen_SaleNotEnded() public { + // Setup sale but don't end it + vm.warp(startTime + 1); + + uint256 depositAmount = MIN_CAP; + usdc.mint(alice, depositAmount); + vm.prank(alice); + usdc.approve(address(sale), depositAmount); + + vm.prank(alice); + sale.deposit(depositAmount); + + // Try to withdraw before ending sale + vm.prank(admin); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.CapNotReached.selector)); + sale.withdrawProceeds(); + } + + function testWithdrawProceeds_RevertWhen_CapNotReached() public { + // Setup failed sale + vm.warp(startTime + 1); + + uint256 depositAmount = MIN_CAP - 1e6; // Just under minimum cap + usdc.mint(alice, depositAmount); + vm.prank(alice); + usdc.approve(address(sale), depositAmount); + vm.prank(alice); + sale.deposit(depositAmount); + + vm.prank(admin); + sale.endSale(); + + // Try to withdraw when cap not reached + vm.prank(admin); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.CapNotReached.selector)); + sale.withdrawProceeds(); + } + + function testWithdrawProceeds_RevertWhen_NotOwner() public { + // Setup successful sale + vm.warp(startTime + 1); + + uint256 depositAmount = MIN_CAP; + usdc.mint(alice, depositAmount); + vm.prank(alice); + usdc.approve(address(sale), depositAmount); + vm.prank(alice); + sale.deposit(depositAmount); + + vm.prank(admin); + sale.endSale(); + + // Try to withdraw from non-owner account + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); + sale.withdrawProceeds(); + } + + function testWithdrawProceeds_MultipleDeposits() public { + // Setup successful sale with multiple deposits + vm.warp(startTime + 1); + + uint256 aliceAmount = MIN_CAP / 2; + uint256 bobAmount = MIN_CAP / 2 + 1e6; // Slightly more to go over MIN_CAP + uint256 totalAmount = aliceAmount + bobAmount; + + // Alice's deposit + usdc.mint(alice, aliceAmount); + vm.prank(alice); + usdc.approve(address(sale), aliceAmount); + vm.prank(alice); + sale.deposit(aliceAmount); + + // Bob's deposit + usdc.mint(bob, bobAmount); + vm.prank(bob); + usdc.approve(address(sale), bobAmount); + vm.prank(bob); + sale.deposit(bobAmount); + + vm.prank(admin); + sale.endSale(); + + // Check initial balances + uint256 initialTreasuryBalance = usdc.balanceOf(TREASURY); + + // Withdraw proceeds + vm.prank(admin); + sale.withdrawProceeds(); + + // Verify full amount was transferred + assertEq(usdc.balanceOf(TREASURY), initialTreasuryBalance + totalAmount); + assertEq(usdc.balanceOf(address(sale)), 0); + } + + function testWithdrawProceeds_MultipleTimes() public { + // Setup successful sale + vm.warp(startTime + 1); + + uint256 depositAmount = MIN_CAP; + usdc.mint(alice, depositAmount); + vm.prank(alice); + usdc.approve(address(sale), depositAmount); + vm.prank(alice); + sale.deposit(depositAmount); + + vm.prank(admin); + sale.endSale(); + + // First withdrawal + vm.prank(admin); + sale.withdrawProceeds(); + assertEq(usdc.balanceOf(TREASURY), depositAmount); + assertEq(usdc.balanceOf(address(sale)), 0); + + // Second withdrawal (should succeed but transfer 0) + vm.prank(admin); + sale.withdrawProceeds(); + assertEq(usdc.balanceOf(TREASURY), depositAmount); // Balance unchanged + assertEq(usdc.balanceOf(address(sale)), 0); + } + + function testWithdrawProceeds_ExactlyAtMinCap() public { + // Setup successful sale exactly at minimum cap + vm.warp(startTime + 1); + + uint256 depositAmount = MIN_CAP; + usdc.mint(alice, depositAmount); + vm.prank(alice); + usdc.approve(address(sale), depositAmount); + vm.prank(alice); + sale.deposit(depositAmount); + + vm.prank(admin); + sale.endSale(); + + uint256 initialTreasuryBalance = usdc.balanceOf(TREASURY); + + // Withdraw proceeds vm.prank(admin); sale.withdrawProceeds(); - assertEq(usdc.balanceOf(TREASURY), MAX_CAP); + assertEq(usdc.balanceOf(TREASURY), initialTreasuryBalance + depositAmount); + assertEq(usdc.balanceOf(address(sale)), 0); } } From 25e205923446c1a3c6ff17cfef87fd6b3446e813 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 17:50:00 -0600 Subject: [PATCH 18/28] Refactor SealedBidTokenSale contract to enhance documentation, improve error messages, and clarify function descriptions for better code readability and maintainability. --- src/apps/SealedBidTokenSale.sol | 131 ++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 47 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index f62dd141..ba4e1ce8 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -9,90 +9,107 @@ import {SafeERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/utils/SafeERC /** * @title SealedBidTokenSale - * @dev Sealed-bid auction-style token sale with USDC deposits and Merkle-based claims. - * Features time-bound participation, minimum/maximum caps, and non-custodial withdrawals. + * @notice Manages a sealed-bid token sale where users deposit USDC and receive tokens based on their allocations + * @dev Implements a non-custodial token sale mechanism with the following features: + * - Time-bound participation window + * - Minimum cap for sale success + * - USDC deposits from users + * - Merkle-based token allocation claims + * - Full refunds if minimum cap not reached */ contract SealedBidTokenSale is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; /* ============ Custom Errors ============ */ - /// @notice Invalid sale token address provided (zero address) + /// @notice Thrown when attempting to initialize with zero address for sale token error InvalidSaleTokenAddress(address token); - /// @notice Invalid treasury address provided (zero address) + /// @notice Thrown when attempting to initialize with zero address for treasury error InvalidTreasuryAddress(address treasury); - /// @notice Sale period has not started yet + /// @notice Thrown when attempting operations before sale start time error SaleNotStarted(uint256 currentTime, uint256 startTime); - /// @notice Sale period has already ended + /// @notice Thrown when attempting operations after sale has ended error SaleAlreadyEnded(uint256 currentTime); - /// @notice Sale period has not ended yet + /// @notice Thrown when attempting operations that require sale to be ended error SaleNotEnded(uint256 currentTime); - /// @notice Operation requires capReached sale state + /// @notice Thrown when attempting operations that require minimum cap to be reached error CapNotReached(); - /// @notice Operation requires failed sale state + /// @notice Thrown when attempting withdrawals in successful sale error SaleWasSuccessful(); - /// @notice Deposited amount must be greater than zero + /// @notice Thrown when attempting to deposit zero amount error ZeroDeposit(); - /// @notice No funds available for withdrawal + /// @notice Thrown when user has no funds to withdraw error NothingToWithdraw(address user); - /// @notice Tokens have already been claimed + /// @notice Thrown when attempting to claim tokens more than once error AlreadyClaimed(address user); - /// @notice Provided Merkle proof is invalid + /// @notice Thrown when provided Merkle proof is invalid error InvalidProof(bytes32[] proof, bytes32 leaf); - /// @notice Merkle root not set for claims + /// @notice Thrown when attempting claims before Merkle root is set error MerkleRootNotSet(); /* ============ Events ============ */ - /// @notice Emitted on USDC deposit + /// @notice Emitted when a user deposits USDC into the sale + /// @param user Address of the depositing user + /// @param amount Amount of USDC deposited event Deposited(address indexed user, uint256 amount); - /// @notice Emitted on USDC withdrawal + + /// @notice Emitted when a user withdraws USDC from a failed sale + /// @param user Address of the withdrawing user + /// @param amount Amount of USDC withdrawn event Withdrawn(address indexed user, uint256 amount); - /// @notice Emitted when sale is ended + + /// @notice Emitted when the sale is officially ended + /// @param capReached Whether the minimum cap was reached + /// @param totalDeposited Total amount of USDC deposited in sale event SaleEnded(bool capReached, uint256 totalDeposited); - /// @notice Emitted when Merkle root is set + + /// @notice Emitted when the Merkle root for token allocations is set + /// @param root New Merkle root value event MerkleRootSet(bytes32 root); - /// @notice Emitted on capReached token claim + + /// @notice Emitted when a user claims their allocated tokens + /// @param user Address of the claiming user + /// @param tokenAmount Amount of tokens claimed event Claimed(address indexed user, uint256 tokenAmount); /* ============ Immutable Parameters ============ */ - /// @notice Sale token contract + /// @notice Token being sold in the sale IERC20 public immutable saleToken; - /// @notice USDC token contract + /// @notice USDC token contract for deposits IERC20 public immutable USDC; - /// @notice Treasury address for proceeds + /// @notice Address where sale proceeds will be sent address public immutable treasury; - /// @notice Sale start timestamp + /// @notice Timestamp when the sale begins uint256 public immutable startTime; - /// @notice Minimum USDC required for success + /// @notice Minimum amount of USDC required for sale success uint256 public immutable minimumCap; /* ============ State Variables ============ */ - /// @notice Sale finalization status + /// @notice Whether the sale period has officially ended bool public saleEnded; - /// @notice Sale success status + /// @notice Whether the minimum cap was reached by end of sale bool public capReached; - /// @notice Total USDC deposited + /// @notice Running total of USDC deposited into sale uint256 public totalDeposited; - /// @notice Merkle root for allocations + /// @notice Merkle root for verifying token allocations bytes32 public merkleRoot; - /// @notice User deposits tracking + /// @notice Maps user addresses to their USDC deposit amounts mapping(address => uint256) public deposits; - /// @notice Claims tracking + /// @notice Maps user addresses to whether they've claimed tokens mapping(address => bool) public hasClaimed; /* ============ Constructor ============ */ /** - * @notice Initialize sale parameters - * @param _saleToken Sale token address - * @param _treasury Treasury address for proceeds - * @param _usdcToken USDC token address - * - * @param _startTime Sale start timestamp - * @param _minimumCap Minimum USDC for success + * @notice Initializes the token sale with required parameters + * @param _saleToken Address of the token being sold + * @param _treasury Address where sale proceeds will be sent + * @param _usdcToken Address of the USDC token contract + * @param _startTime Timestamp when sale will begin + * @param _minimumCap Minimum USDC amount required for sale success */ constructor(address _saleToken, address _treasury, address _usdcToken, uint256 _startTime, uint256 _minimumCap) Ownable(msg.sender) @@ -110,7 +127,11 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ User Functions ============ */ /** - * @notice Deposited USDC into the sale + * @notice Allows users to deposit USDC into the token sale + * @dev - Sale must be active (after start time, before end) + * - Amount must be greater than zero + * - Updates user's deposit balance and total deposits + * - Transfers USDC from user to contract * @param amount Amount of USDC to deposit */ function deposit(uint256 amount) external nonReentrant { @@ -127,7 +148,11 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { } /** - * @notice Withdrawn USDC if sale failed + * @notice Allows users to withdraw their USDC if sale failed to reach minimum cap + * @dev - Sale must be ended and minimum cap not reached + * - User must have deposited USDC + * - Sends full deposit amount back to user + * - Sets user's deposit balance to zero */ function withdraw() external nonReentrant { if (!saleEnded) revert SaleNotEnded(block.timestamp); @@ -143,10 +168,15 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { } /** - * @notice Claimed allocated tokens using Merkle proof - * @param saleTokenAllocation Token amount allocated to sender - * @param proof Merkle proof for allocation - * @param user User to claim tokens for + * @notice Allows users to claim their allocated tokens using a Merkle proof + * @dev - Sale must be ended successfully and Merkle root set + * - User must not have claimed already + * - Proof must be valid for user's allocation + * - Transfers allocated tokens and USDC to user + * @param saleTokenAllocation Amount of sale tokens allocated to user + * @param usdcAllocation Amount of USDC allocated to user + * @param proof Merkle proof verifying the allocation + * @param user Address of user claiming tokens */ function claimTokens(uint256 saleTokenAllocation, uint256 usdcAllocation, bytes32[] calldata proof, address user) external @@ -175,7 +205,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ Admin Functions ============ */ /** - * @notice Ends sale + * @notice Allows owner to officially end the sale + * @dev - Can only be called once + * - Sets final sale status based on minimum cap + * - Emits event with final sale results */ function endSale() external onlyOwner { if (saleEnded) revert SaleAlreadyEnded(block.timestamp); @@ -187,8 +220,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { } /** - * @notice Set Merkle root for allocations - * @param newRoot Root of allocation Merkle tree + * @notice Sets the Merkle root for verifying token allocations + * @dev - Sale must be ended successfully + * - Enables token claiming process + * @param newRoot The Merkle root hash of all valid allocations */ function setMerkleRoot(bytes32 newRoot) external onlyOwner { if (!saleEnded || !capReached) revert CapNotReached(); @@ -197,7 +232,9 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { } /** - * @notice Withdrawn proceeds to treasury + * @notice Allows owner to withdraw sale proceeds to treasury + * @dev - Sale must be ended successfully + * - Transfers all USDC to treasury address */ function withdrawProceeds() external onlyOwner { if (!saleEnded || !capReached) revert CapNotReached(); From 910790696faa35a90256b2c83f99dcebdd2d10f5 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 17:53:53 -0600 Subject: [PATCH 19/28] Refactor error handling and add comments for clarity in SealedBidTokenSale contract and update corresponding test case. --- src/apps/SealedBidTokenSale.sol | 33 ++++++++++++++++++++++--- test/unit/apps/SealedBidTokenSale.t.sol | 2 +- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index ba4e1ce8..7956050d 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -34,8 +34,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error SaleNotEnded(uint256 currentTime); /// @notice Thrown when attempting operations that require minimum cap to be reached error CapNotReached(); - /// @notice Thrown when attempting withdrawals in successful sale - error SaleWasSuccessful(); + /// @notice Thrown when attempting withdrawals if cap is reached + error CapReached(); /// @notice Thrown when attempting to deposit zero amount error ZeroDeposit(); /// @notice Thrown when user has no funds to withdraw @@ -135,15 +135,19 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param amount Amount of USDC to deposit */ function deposit(uint256 amount) external nonReentrant { + // Verify sale is active and deposit is valid if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); if (saleEnded) revert SaleAlreadyEnded(block.timestamp); if (amount == 0) revert ZeroDeposit(); + // Update deposit accounting deposits[msg.sender] += amount; totalDeposited += amount; + // Transfer USDC from user to contract USDC.safeTransferFrom(msg.sender, address(this), amount); + // Emit deposit event emit Deposited(msg.sender, amount); } @@ -155,15 +159,21 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * - Sets user's deposit balance to zero */ function withdraw() external nonReentrant { + // Verify sale has ended unsuccessfully if (!saleEnded) revert SaleNotEnded(block.timestamp); - if (capReached) revert SaleWasSuccessful(); + if (capReached) revert CapReached(); + // Get user's deposit amount uint256 amount = deposits[msg.sender]; if (amount == 0) revert NothingToWithdraw(msg.sender); + // Clear user's deposit before transfer deposits[msg.sender] = 0; + + // Return USDC to user USDC.safeTransfer(msg.sender, amount); + // Emit withdrawal event emit Withdrawn(msg.sender, amount); } @@ -182,23 +192,29 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { external nonReentrant { + // Verify sale ended successfully and claims are enabled if (!saleEnded || !capReached) revert CapNotReached(); if (merkleRoot == bytes32(0)) revert MerkleRootNotSet(); if (hasClaimed[user]) revert AlreadyClaimed(user); + // Create and verify Merkle leaf bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(user, saleTokenAllocation, usdcAllocation)))); if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof(proof, leaf); + // Mark as claimed before transfers hasClaimed[user] = true; + // Transfer allocated sale tokens if any if (saleTokenAllocation > 0) { saleToken.safeTransfer(user, saleTokenAllocation); } + // Transfer allocated USDC if any if (usdcAllocation > 0) { USDC.safeTransfer(user, usdcAllocation); } + // Emit claim event emit Claimed(user, saleTokenAllocation); } @@ -211,11 +227,14 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * - Emits event with final sale results */ function endSale() external onlyOwner { + // Verify sale hasn't already been ended if (saleEnded) revert SaleAlreadyEnded(block.timestamp); + // Mark sale as ended and determine if cap was reached saleEnded = true; capReached = totalDeposited >= minimumCap; + // Emit sale end event with final status emit SaleEnded(capReached, totalDeposited); } @@ -226,8 +245,13 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param newRoot The Merkle root hash of all valid allocations */ function setMerkleRoot(bytes32 newRoot) external onlyOwner { + // Verify sale ended successfully before setting root if (!saleEnded || !capReached) revert CapNotReached(); + + // Update Merkle root merkleRoot = newRoot; + + // Emit root update event emit MerkleRootSet(newRoot); } @@ -237,7 +261,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * - Transfers all USDC to treasury address */ function withdrawProceeds() external onlyOwner { + // Verify sale ended successfully if (!saleEnded || !capReached) revert CapNotReached(); + + // Transfer all USDC balance to treasury USDC.safeTransfer(treasury, USDC.balanceOf(address(this))); } } diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 453dd3bd..f7e75988 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -435,7 +435,7 @@ contract SealedBidTokenSaleTest is SharedSetup { // Attempt withdrawal on successful sale vm.prank(alice); - vm.expectRevert(SealedBidTokenSale.SaleWasSuccessful.selector); + vm.expectRevert(SealedBidTokenSale.CapReached.selector); sale.withdraw(); } From 2c16f052110b8256ac6ad9e455aaa1f636ccdcc1 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 17:56:19 -0600 Subject: [PATCH 20/28] Refactor SealedBidTokenSale: simplify timing, remove max cap, add dual allocations, optimize gas, and enhance security with reentrancy protection. --- src/apps/SealedBidTokenSale.md | 235 +++++++++++++++------------------ 1 file changed, 106 insertions(+), 129 deletions(-) diff --git a/src/apps/SealedBidTokenSale.md b/src/apps/SealedBidTokenSale.md index c8de2233..c89c6e0a 100644 --- a/src/apps/SealedBidTokenSale.md +++ b/src/apps/SealedBidTokenSale.md @@ -1,15 +1,14 @@ # SealedBidTokenSale Technical Specification -Below is the **technical specification** for a Solidity **sealed‐bid, multi‐unit uniform‐price** token sale contract. - -All other core features—time‐limited deposits in USDC, minimum/maximum cap checks, finalization logic, off‐chain price discovery, Merkle‐based token claims, and refunds if the sale fails—remain consistent with the design. +Below is the **technical specification** for a Solidity **sealed‐bid** token sale contract based on the implemented code. --- ## 1. **Contract Overview** - **Name**: `SealedBidTokenSale` -- **Purpose**: Accept USDC deposits for an off‐chain price discovery token sale, enforce timing and caps, enable refunds if the sale is unsuccessful, and distribute tokens (via a Merkle proof) if successful. +- **Purpose**: Accept USDC deposits for a token sale, enforce timing and minimum cap requirements, enable refunds if the sale is unsuccessful, and distribute tokens and USDC allocations via a Merkle proof if successful. +- **Inheritance**: `Ownable`, `ReentrancyGuard` --- @@ -17,193 +16,171 @@ All other core features—time‐limited deposits in USDC, minimum/maximum cap c - **Owner**: - Inherits from OpenZeppelin `Ownable`. - - The owner sets crucial parameters during contract deployment. - - The owner finalizes the sale, sets the Merkle root, and withdraws proceeds to the treasury address. + - Sets crucial parameters during contract deployment. + - Controls sale finalization, Merkle root setting, and proceeds withdrawal. - **Participants**: - Deposit USDC during the sale window. - Withdraw their deposit if the sale fails. - - Claim tokens after the sale succeeds, using a Merkle proof. + - Claim tokens and USDC allocations after sale success using a Merkle proof. - **Treasury**: - - A predetermined address specified at contract deployment. - - Receives the USDC proceeds if the sale succeeds. + - Immutable address specified at deployment. + - Receives USDC proceeds upon successful sale completion. --- -## 3. **Immutable & Configurable Parameters** +## 3. **Immutable Parameters** -1. **`Ownable`** - - The contract is governed by an owner (`owner()`) managed by OpenZeppelin’s `Ownable`. +1. **`saleToken`** (`IERC20`) + - Token being sold through the contract. + - Set at construction. -2. **`treasury`** (`address`) - - Set at construction. - - **Immutable**; where funds are sent on success. +2. **`USDC`** (`IERC20`) + - USDC token contract reference for deposits. + - Set at construction. -3. **`usdcToken`** (`IERC20`) - - The address of the USDC contract. - - Used for `transferFrom` and `transfer` calls. +3. **`treasury`** (`address`) + - Fixed address that receives proceeds. + - Set at construction. -4. **`startTime`** and **`endTime`** (`uint256`) - - The sale window during which deposits are accepted. +4. **`startTime`** (`uint256`) + - Sale start timestamp. + - Set at construction. 5. **`minimumCap`** (`uint256`) - - The minimum total USDC deposit threshold required for the sale to succeed. - -6. **`maximumCap`** (`uint256`) - - The maximum total USDC deposit allowed; can be `0` if no maximum limit is needed. + - Minimum USDC required for success. + - Set at construction. --- ## 4. **State Variables** -1. **`finalized`** (`bool`) - - Indicates if the sale has been finalized. - - Once set, the contract’s deposit logic is locked. +1. **`saleEnded`** (`bool`) + - Indicates if owner has ended the sale. -2. **`successful`** (`bool`) - - `true` if `totalDeposited >= minimumCap` upon finalization. - - Determines whether users can claim tokens or must withdraw refunds. +2. **`capReached`** (`bool`) + - Set to `true` if `totalDeposited >= minimumCap` when sale ends. 3. **`totalDeposited`** (`uint256`) - - Running total of all USDC deposits received. + - Sum of all USDC deposits. -4. **`deposits`** (`mapping(address => uint256)`) - - Tracks each participant’s cumulative deposit. +4. **`merkleRoot`** (`bytes32`) + - Root hash for token and USDC allocation proofs. -5. **`merkleRoot`** (`bytes32`) - - Root hash of an off‐chain Merkle tree that encodes final token allocations. +5. **`deposits`** (`mapping(address => uint256)`) + - Tracks each user's USDC deposit amount. 6. **`hasClaimed`** (`mapping(address => bool)`) - - Tracks whether a participant has already claimed tokens. + - Records whether an address has claimed their allocation. --- ## 5. **Core Functions** ### 5.1 **`deposit(uint256 amount)`** -- **Purpose**: Allows participants to deposit USDC into the sale, multiple times if desired. +- **Purpose**: Accepts USDC deposits from participants. - **Constraints**: - 1. Must be called after `startTime` and before `endTime`. - 2. Sale must not be `finalized`. - 3. The `amount` must be non-zero. - 4. If `maximumCap > 0`, `totalDeposited + amount <= maximumCap`. - 5. Transfers USDC using `transferFrom(msg.sender, address(this), amount)`. + 1. Must be after `startTime`. + 2. Sale must not be ended. + 3. Amount must be non-zero. - **Effects**: - - Increments `deposits[msg.sender]` and `totalDeposited`. - - Emits a `Deposit` event. + - Updates `deposits[msg.sender]` and `totalDeposited`. + - Transfers USDC from sender to contract. + - Emits `Deposited` event. ### 5.2 **`withdraw()`** -- **Purpose**: Allows participants to **refund** their USDC deposits if the sale fails. +- **Purpose**: Returns USDC to depositors if sale fails. - **Constraints**: - 1. Can only be called after `finalized`. - 2. Only possible if `successful == false`. - 3. Caller’s `deposits[msg.sender]` must be > 0. + 1. Sale must be ended. + 2. Cap must not be reached. + 3. Caller must have non-zero deposit. - **Effects**: - - Refunds the user’s entire deposit via `transfer`. - - Sets `deposits[msg.sender] = 0` to prevent re‐entrancy. - - Emits a `Withdraw` event. + - Returns user's entire USDC deposit. + - Zeroes their deposit balance. + - Emits `Withdrawn` event. -### 5.3 **`finalize()`** (Owner‐only) -- **Purpose**: Ends the deposit phase, locks in whether the sale is successful, and stops further deposits. +### 5.3 **`endSale()`** (Owner-only) +- **Purpose**: Finalizes sale and determines success. - **Constraints**: - 1. Can only be called by the **owner**. - 2. Must not be already `finalized`. - 3. Typically requires current time >= `endTime` **or** `totalDeposited == maximumCap` (if the cap is exhausted early). + 1. Only callable by owner. + 2. Sale must not already be ended. - **Effects**: - - Sets `finalized = true`. - - Sets `successful = (totalDeposited >= minimumCap)`. - - Emits a `Finalized` event with the final outcome. + - Sets `saleEnded = true`. + - Sets `capReached` based on minimum cap check. + - Emits `SaleEnded` event. -### 5.4 **`setMerkleRoot(bytes32 _merkleRoot)`** (Owner‐only) -- **Purpose**: Records the final allocations in a **Merkle root** for off‐chain computed distribution. +### 5.4 **`claimTokens(uint256 saleTokenAllocation, uint256 usdcAllocation, bytes32[] calldata proof, address user)`** +- **Purpose**: Processes token and USDC claims using Merkle proofs. - **Constraints**: - 1. Must be called by the **owner**. - 2. The sale must be `finalized` and `successful`. + 1. Sale must be ended and successful. + 2. Merkle root must be set. + 3. User must not have claimed. + 4. Valid Merkle proof required. - **Effects**: - - Updates `merkleRoot` to `_merkleRoot`. - - Emits a `MerkleRootSet` event. + - Marks user as claimed. + - Transfers allocated sale tokens. + - Returns allocated USDC. + - Emits `Claimed` event. -### 5.5 **`claimTokens(uint256 allocation, bytes32[] calldata proof)`** -- **Purpose**: Lets each participant claim their allocated tokens (as computed off‐chain), verified by a **Merkle proof**. +### 5.5 **`setMerkleRoot(bytes32 newRoot)`** (Owner-only) +- **Purpose**: Sets allocation Merkle root. - **Constraints**: - 1. The sale must be `finalized` and `successful`. - 2. A valid `merkleRoot` must be set. - 3. `hasClaimed[msg.sender] == false` (no double‐claim). - 4. The `(address, allocation)` leaf must be verified against `merkleRoot` using `MerkleProof.verify`. + 1. Sale must be ended and successful. + 2. Only callable by owner. - **Effects**: - - Marks `hasClaimed[msg.sender] = true`. - - **Transfers** (or **mints**) `allocation` tokens to the caller. - - Emits a `Claim` event. + - Sets `merkleRoot`. + - Emits `MerkleRootSet` event. -### 5.6 **`withdrawProceeds()`** (Owner‐only) -- **Purpose**: Transfers **all** USDC proceeds to the **predetermined `treasury`** address if the sale is successful. +### 5.6 **`withdrawProceeds()`** (Owner-only) +- **Purpose**: Sends USDC to treasury. - **Constraints**: - 1. Must be called by the **owner**. - 2. The sale must be `finalized` and `successful`. + 1. Sale must be ended and successful. + 2. Only callable by owner. - **Effects**: - - Transfers the entire USDC balance from the contract to `treasury`. + - Transfers all USDC to treasury address. --- -## 6. **Life Cycle** - -1. **Deployment** - - Deployed with constructor parameters, including `treasury`, `startTime`, `endTime`, `minimumCap`, `maximumCap`. - - The contract references the USDC address for deposits. +## 6. **Custom Errors** -2. **Deposit Phase** - - Participants call `deposit(amount)` any number of times from `startTime` to `endTime` (unless `maximumCap` is reached). - - `totalDeposited` is aggregated. +1. **Parameter Validation**: + - `InvalidSaleTokenAddress` + - `InvalidTreasuryAddress` + - `ZeroDeposit` -3. **Finalization** - - After `endTime` (or upon reaching `maximumCap`), the owner calls `finalize()`. - - The contract determines `successful` based on `minimumCap`. +2. **State Checks**: + - `SaleNotStarted` + - `SaleAlreadyEnded` + - `SaleNotEnded` + - `CapNotReached` + - `SaleWasSuccessful` -4. **Outcomes** - - **Unsuccessful**: If `totalDeposited < minimumCap`, participants can call `withdraw()` to get refunds. - - **Successful**: - - The owner sets a `merkleRoot` to define each user’s final token allocation. - - Participants use `claimTokens(allocation, proof)` to claim tokens. - - The owner can call `withdrawProceeds()` to send USDC to the `treasury`. +3. **Claim Validation**: + - `NothingToWithdraw` + - `AlreadyClaimed` + - `InvalidProof` + - `MerkleRootNotSet` --- -## 7. **Implementation Considerations** - -1. **Token Distribution Mechanism** - - The contract must hold or be able to mint the tokens for `claimTokens()`. - - This might involve transferring tokens in advance or using a mint function in an external token contract. - -2. **Security** - - Use **OpenZeppelin** libraries (`Ownable`, `ReentrancyGuard`, `MerkleProof`) for best practices. - - Validate deposit calls to prevent deposits outside the allowed window. - - Carefully handle refunds (set user deposit to 0 before transferring USDC back). +## 7. **Key Design Changes** -3. **Edge Cases** - - If `maximumCap == 0`, only time gating applies. - - If participants deposit after `maximumCap` is reached, the contract must revert. - - The owner might finalize **early** if `totalDeposited == maximumCap` before `endTime`. - - If `startTime` equals `endTime` or if `_endTime <= _startTime`, the constructor should revert. - -4. **No Immediate Secondary Trading** - - The specification assumes tokens are **not** tradable until after the sale. - - Participants may hold or wait to claim tokens; however, that is outside the core on‐chain deposit/refund logic. - -5. **Custom Errors** - - Reverts use 0.8‐style **custom errors** for gas efficiency (e.g., `error SaleNotStarted();`, `error SaleEnded();`, etc.). - ---- +1. **Simplified Timing**: + - Only enforces start time, not end time. + - Owner controls sale end via `endSale()`. -## 8. **Final Takeaway** +2. **Dual Allocations**: + - Claims include both token and USDC amounts. + - Merkle proofs verify both allocations together. -This specification establishes a **time‐bound, USDC‐based deposit system** with: +3. **Removed Maximum Cap**: + - No upper limit on total deposits. + - Sale ends only through owner action. -- A **minimum funding threshold** (`minimumCap`) for success. -- An **optional maximum** (`maximumCap`). -- **Finalization** by the owner. -- **Refunds** if not successful. -- **Merkle‐based claims** if successful. -- **Proceeds** withdrawn by the owner to a **fixed `treasury` address**. +4. **Gas Optimizations**: + - Uses OpenZeppelin's `SafeERC20`. + - Implements custom errors. + - Includes reentrancy protection. -All **off‐chain** bid details and final allocation logic remain external; the contract only enforces **deposits**, **caps**, **timing**, and **fund distribution**, while using a **Merkle tree** for post‐sale token allocation. +This implementation provides a flexible token sale mechanism with owner-controlled timing, minimum success threshold, and Merkle-based dual-asset distribution. From 3e6417d0caab49bb6a98dc77e3870a8a78cbfb45 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Mon, 3 Feb 2025 17:58:43 -0600 Subject: [PATCH 21/28] Removed the "Key Design Changes" section from SealedBidTokenSale.md, simplifying the technical specification and focusing on the core contract details. --- src/apps/SealedBidTokenSale.md | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/apps/SealedBidTokenSale.md b/src/apps/SealedBidTokenSale.md index c89c6e0a..f88e0caa 100644 --- a/src/apps/SealedBidTokenSale.md +++ b/src/apps/SealedBidTokenSale.md @@ -1,6 +1,6 @@ # SealedBidTokenSale Technical Specification -Below is the **technical specification** for a Solidity **sealed‐bid** token sale contract based on the implemented code. +Below is the **technical specification** for a Solidity **sealed‐bid** token sale contract. --- @@ -162,25 +162,3 @@ Below is the **technical specification** for a Solidity **sealed‐bid** token s - `InvalidProof` - `MerkleRootNotSet` ---- - -## 7. **Key Design Changes** - -1. **Simplified Timing**: - - Only enforces start time, not end time. - - Owner controls sale end via `endSale()`. - -2. **Dual Allocations**: - - Claims include both token and USDC amounts. - - Merkle proofs verify both allocations together. - -3. **Removed Maximum Cap**: - - No upper limit on total deposits. - - Sale ends only through owner action. - -4. **Gas Optimizations**: - - Uses OpenZeppelin's `SafeERC20`. - - Implements custom errors. - - Includes reentrancy protection. - -This implementation provides a flexible token sale mechanism with owner-controlled timing, minimum success threshold, and Merkle-based dual-asset distribution. From 2d2c705fa75119dbaca30db9e238d6bceecefce1 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Tue, 4 Feb 2025 09:25:23 -0600 Subject: [PATCH 22/28] Add SaleInfo struct and saleStatus function to SealedBidTokenSale contract, update state variables, and enhance unit tests for comprehensive sale status tracking. --- src/apps/SealedBidTokenSale.sol | 46 ++++++++ test/unit/apps/SealedBidTokenSale.t.sol | 134 ++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 7956050d..b51cf0de 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -20,6 +20,22 @@ import {SafeERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/utils/SafeERC contract SealedBidTokenSale is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; + /* ============ Struct ============ */ + + struct SaleInfo { + uint256 startTime; + uint256 minimumCap; + uint256 totalDeposited; + uint256 totalWithdrawn; + uint256 totalUsdcClaimed; + uint256 totalSaleTokenClaimed; + bool saleEnded; + bool capReached; + bool hasClaimed; + uint256 depositAmount; + uint256 contributorCount; + } + /* ============ Custom Errors ============ */ /// @notice Thrown when attempting to initialize with zero address for sale token @@ -92,14 +108,22 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { bool public saleEnded; /// @notice Whether the minimum cap was reached by end of sale bool public capReached; + /// @notice Running total of USDC withdrawn from sale + uint256 public totalWithdrawn; /// @notice Running total of USDC deposited into sale uint256 public totalDeposited; + /// @notice Running total of USDC claimed from sale + uint256 public totalUsdcClaimed; + /// @notice Running total of sale token withdrawn from sale + uint256 public totalSaleTokenClaimed; /// @notice Merkle root for verifying token allocations bytes32 public merkleRoot; /// @notice Maps user addresses to their USDC deposit amounts mapping(address => uint256) public deposits; /// @notice Maps user addresses to whether they've claimed tokens mapping(address => bool) public hasClaimed; + /// @notice Count of all contributors + uint256 public contributorCount; /* ============ Constructor ============ */ @@ -143,6 +167,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { // Update deposit accounting deposits[msg.sender] += amount; totalDeposited += amount; + contributorCount++; // Transfer USDC from user to contract USDC.safeTransferFrom(msg.sender, address(this), amount); @@ -169,6 +194,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { // Clear user's deposit before transfer deposits[msg.sender] = 0; + totalWithdrawn += amount; // Return USDC to user USDC.safeTransfer(msg.sender, amount); @@ -207,11 +233,13 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { // Transfer allocated sale tokens if any if (saleTokenAllocation > 0) { saleToken.safeTransfer(user, saleTokenAllocation); + totalSaleTokenClaimed += saleTokenAllocation; } // Transfer allocated USDC if any if (usdcAllocation > 0) { USDC.safeTransfer(user, usdcAllocation); + totalUsdcClaimed += usdcAllocation; } // Emit claim event @@ -267,4 +295,22 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { // Transfer all USDC balance to treasury USDC.safeTransfer(treasury, USDC.balanceOf(address(this))); } + + /* ============ View Functions ============ */ + + function saleStatus(address user) external view returns (SaleInfo memory) { + return SaleInfo({ + startTime: startTime, + minimumCap: minimumCap, + totalDeposited: totalDeposited, + totalWithdrawn: totalWithdrawn, + totalUsdcClaimed: totalUsdcClaimed, + totalSaleTokenClaimed: totalSaleTokenClaimed, + saleEnded: saleEnded, + capReached: capReached, + hasClaimed: hasClaimed[user], + depositAmount: deposits[user], + contributorCount: contributorCount + }); + } } diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index f7e75988..96f58b09 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -1036,4 +1036,138 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(usdc.balanceOf(TREASURY), initialTreasuryBalance + depositAmount); assertEq(usdc.balanceOf(address(sale)), 0); } + + /* ============ saleStatus ============ */ + + function testSaleStatus_InitialState() public view { + // Check initial state + SealedBidTokenSale.SaleInfo memory info = sale.saleStatus(alice); + + assertEq(info.startTime, startTime, "Start time should match constructor"); + assertEq(info.minimumCap, MIN_CAP, "Minimum cap should match constructor"); + assertEq(info.totalDeposited, 0, "Total deposited should be 0"); + assertEq(info.totalWithdrawn, 0, "Total withdrawn should be 0"); + assertEq(info.totalUsdcClaimed, 0, "Total USDC claimed should be 0"); + assertEq(info.totalSaleTokenClaimed, 0, "Total sale token claimed should be 0"); + assertEq(info.saleEnded, false, "Sale should not be ended"); + assertEq(info.capReached, false, "Cap should not be reached"); + assertEq(info.hasClaimed, false, "User should not have claimed"); + assertEq(info.depositAmount, 0, "User deposit amount should be 0"); + assertEq(info.contributorCount, 0, "Contributor count should be 0"); + } + + function testSaleStatus_AfterDeposit() public { + // Setup deposit + vm.warp(startTime + 1); + uint256 amount = 1000 * 1e6; + + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + // Make deposit + vm.prank(alice); + sale.deposit(amount); + + // Check state after deposit + SealedBidTokenSale.SaleInfo memory info = sale.saleStatus(alice); + + assertEq(info.totalDeposited, amount, "Total deposited should match deposit"); + assertEq(info.depositAmount, amount, "User deposit should match deposit"); + assertEq(info.contributorCount, 1, "Contributor count should be 1"); + } + + function testSaleStatus_AfterWithdraw() public { + // Setup deposit + vm.warp(startTime + 1); + uint256 amount = 1000 * 1e6; + + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.prank(alice); + sale.deposit(amount); + + // End sale without reaching cap + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + // Withdraw + vm.prank(alice); + sale.withdraw(); + + // Check state after withdrawal + SealedBidTokenSale.SaleInfo memory info = sale.saleStatus(alice); + + assertEq(info.totalWithdrawn, amount, "Total withdrawn should match deposit"); + assertEq(info.depositAmount, 0, "User deposit should be 0 after withdrawal"); + assertEq(info.saleEnded, true, "Sale should be ended"); + } + + function testSaleStatus_AfterSuccessfulSaleAndClaim() public { + // Setup successful sale + vm.warp(startTime + 1); + + usdc.mint(alice, MAX_CAP); + saleToken.mint(address(sale), saleTokenAllocation); + + vm.prank(alice); + usdc.approve(address(sale), MAX_CAP); + + vm.prank(alice); + sale.deposit(MAX_CAP); + + // End sale + vm.warp(endTime); + vm.prank(admin); + sale.endSale(); + + // Set merkle root and claim + vm.prank(admin); + sale.setMerkleRoot(merkleRoot); + + sale.claimTokens(saleTokenAllocation, usdcAllocation, proof, alice); + + // Check state after claim + SealedBidTokenSale.SaleInfo memory info = sale.saleStatus(alice); + + assertEq(info.totalUsdcClaimed, usdcAllocation, "Total USDC claimed should match allocation"); + assertEq(info.totalSaleTokenClaimed, saleTokenAllocation, "Total sale token claimed should match allocation"); + assertEq(info.saleEnded, true, "Sale should be ended"); + assertEq(info.capReached, true, "Cap should be reached"); + assertEq(info.hasClaimed, true, "User should have claimed"); + } + + function testSaleStatus_MultipleUsers() public { + // Setup deposits for multiple users + vm.warp(startTime + 1); + uint256 amount = 1000 * 1e6; + + // Setup Alice + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + vm.prank(alice); + sale.deposit(amount); + + // Setup Bob + usdc.mint(bob, amount); + vm.prank(bob); + usdc.approve(address(sale), amount); + vm.prank(bob); + sale.deposit(amount); + + // Check states for both users + SealedBidTokenSale.SaleInfo memory aliceInfo = sale.saleStatus(alice); + SealedBidTokenSale.SaleInfo memory bobInfo = sale.saleStatus(bob); + + assertEq(aliceInfo.totalDeposited, amount * 2, "Total deposited should include both deposits"); + assertEq(bobInfo.totalDeposited, amount * 2, "Total deposited should be same for all users"); + assertEq(aliceInfo.depositAmount, amount, "Alice deposit should match her deposit"); + assertEq(bobInfo.depositAmount, amount, "Bob deposit should match his deposit"); + assertEq(aliceInfo.contributorCount, 2, "Contributor count should be 2"); + assertEq(bobInfo.contributorCount, 2, "Contributor count should be same for all users"); + } } From c94a69518f8b450321b144b2a12a24afcf6d9cf0 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Tue, 4 Feb 2025 16:02:07 -0600 Subject: [PATCH 23/28] Add maxPrice functionality to SealedBidTokenSale contract and update tests accordingly. --- src/apps/SealedBidTokenSale.sol | 36 +++- test/unit/apps/SealedBidTokenSale.t.sol | 225 +++++++++++++++++++----- 2 files changed, 211 insertions(+), 50 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index b51cf0de..24dc450a 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -32,8 +32,9 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { bool saleEnded; bool capReached; bool hasClaimed; - uint256 depositAmount; uint256 contributorCount; + uint256 depositAmount; + uint256 maxPrice; } /* ============ Custom Errors ============ */ @@ -89,6 +90,13 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /// @param tokenAmount Amount of tokens claimed event Claimed(address indexed user, uint256 tokenAmount); + // Add this event to the SealedBidTokenSale contract's events section + /// @notice Emitted when a user updates their max price + /// @param user Address of the user updating their max price + /// @param oldPrice Previous max price value + /// @param newPrice New max price value + event MaxPriceUpdated(address indexed user, uint256 oldPrice, uint256 newPrice); + /* ============ Immutable Parameters ============ */ /// @notice Token being sold in the sale @@ -122,6 +130,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { mapping(address => uint256) public deposits; /// @notice Maps user addresses to whether they've claimed tokens mapping(address => bool) public hasClaimed; + /// @notice Maps user addresses to their selected maxPrice + mapping(address => uint256) public maxPrices; /// @notice Count of all contributors uint256 public contributorCount; @@ -154,11 +164,12 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @notice Allows users to deposit USDC into the token sale * @dev - Sale must be active (after start time, before end) * - Amount must be greater than zero - * - Updates user's deposit balance and total deposits + * - Updates user's deposit balance, total deposits, and maxPrice * - Transfers USDC from user to contract * @param amount Amount of USDC to deposit + * @param maxPrice The maximum price set by the user for the token sale */ - function deposit(uint256 amount) external nonReentrant { + function deposit(uint256 amount, uint256 maxPrice) external nonReentrant { // Verify sale is active and deposit is valid if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); if (saleEnded) revert SaleAlreadyEnded(block.timestamp); @@ -169,6 +180,9 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { totalDeposited += amount; contributorCount++; + // Save the user's maxPrice + maxPrices[msg.sender] = maxPrice; + // Transfer USDC from user to contract USDC.safeTransferFrom(msg.sender, address(this), amount); @@ -246,6 +260,19 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { emit Claimed(user, saleTokenAllocation); } + /** + * @notice Allows users to update their selected maxPrice for the token sale. + * @param newMaxPrice The new maximum price value to be set for the user. + */ + function updateMaxPrice(uint256 newMaxPrice) external nonReentrant { + if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); + if (saleEnded) revert SaleAlreadyEnded(block.timestamp); + + uint256 oldPrice = maxPrices[msg.sender]; + maxPrices[msg.sender] = newMaxPrice; + emit MaxPriceUpdated(msg.sender, oldPrice, newMaxPrice); + } + /* ============ Admin Functions ============ */ /** @@ -310,7 +337,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { capReached: capReached, hasClaimed: hasClaimed[user], depositAmount: deposits[user], - contributorCount: contributorCount + contributorCount: contributorCount, + maxPrice: maxPrices[user] }); } } diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 96f58b09..e108fa8a 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -24,6 +24,7 @@ contract SealedBidTokenSaleTest is SharedSetup { bytes32[] public proof; uint256 public saleTokenAllocation = 1000 * 1e18; uint256 public usdcAllocation = 1000 * 1e6; + uint256 public maxPrice = 1000 * 1e6; function setUp() public override { super.setUp(); @@ -143,19 +144,20 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), amount); - // Deposit through Kinto Wallet + // Deposit with maxPrice vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); assertEq(sale.deposits(alice), amount); assertEq(sale.totalDeposited(), amount); assertEq(usdc.balanceOf(address(sale)), amount); + assertEq(sale.maxPrices(alice), maxPrice); } function testDeposit_RevertWhen_BeforeStart() public { vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, block.timestamp, startTime)); vm.prank(alice); - sale.deposit(100 ether); + sale.deposit(100 ether, maxPrice); } function testDeposit_RevertWhen_SaleEnded() public { @@ -174,7 +176,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleAlreadyEnded.selector, block.timestamp)); - sale.deposit(amount); + sale.deposit(amount, maxPrice); } function testDeposit_RevertWhen_ZeroAmount() public { @@ -184,7 +186,7 @@ contract SealedBidTokenSaleTest is SharedSetup { // Try to deposit zero amount vm.prank(alice); vm.expectRevert(SealedBidTokenSale.ZeroDeposit.selector); - sale.deposit(0); + sale.deposit(0, maxPrice); } function testDeposit_MultipleDeposits() public { @@ -202,16 +204,17 @@ contract SealedBidTokenSaleTest is SharedSetup { // Make first deposit vm.prank(alice); - sale.deposit(firstAmount); + sale.deposit(firstAmount, maxPrice); // Make second deposit vm.prank(alice); - sale.deposit(secondAmount); + sale.deposit(secondAmount, maxPrice * 2); // Verify final state assertEq(sale.deposits(alice), totalAmount); assertEq(sale.totalDeposited(), totalAmount); assertEq(usdc.balanceOf(address(sale)), totalAmount); + assertEq(sale.maxPrices(alice), maxPrice * 2); } function testDeposit_MultipleUsers() public { @@ -234,16 +237,19 @@ contract SealedBidTokenSaleTest is SharedSetup { // Make deposits vm.prank(alice); - sale.deposit(aliceAmount); + sale.deposit(aliceAmount, maxPrice * 2); vm.prank(bob); - sale.deposit(bobAmount); + sale.deposit(bobAmount, maxPrice); // Verify final state assertEq(sale.deposits(alice), aliceAmount); assertEq(sale.deposits(bob), bobAmount); + assertEq(sale.maxPrices(alice), maxPrice * 2); assertEq(sale.totalDeposited(), totalAmount); assertEq(usdc.balanceOf(address(sale)), totalAmount); + assertEq(sale.maxPrices(bob), maxPrice); + assertEq(sale.maxPrices(alice), maxPrice * 2); } /* ============ endSale ============ */ @@ -257,7 +263,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); vm.prank(admin); sale.endSale(); @@ -276,7 +282,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // End sale vm.prank(admin); @@ -297,7 +303,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // End sale vm.prank(admin); @@ -354,7 +360,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), aliceAmount); vm.prank(alice); - sale.deposit(aliceAmount); + sale.deposit(aliceAmount, maxPrice); // Bob's deposit usdc.mint(bob, bobAmount); @@ -362,7 +368,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), bobAmount); vm.prank(bob); - sale.deposit(bobAmount); + sale.deposit(bobAmount, maxPrice); // End sale vm.prank(admin); @@ -386,7 +392,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); vm.warp(endTime); vm.prank(admin); @@ -409,7 +415,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // Attempt withdrawal before sale ends vm.prank(alice); @@ -427,7 +433,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // End sale successfully vm.prank(admin); @@ -463,7 +469,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), aliceAmount); vm.prank(alice); - sale.deposit(aliceAmount); + sale.deposit(aliceAmount, maxPrice); // Setup and execute Bob's deposit usdc.mint(bob, bobAmount); @@ -471,7 +477,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), bobAmount); vm.prank(bob); - sale.deposit(bobAmount); + sale.deposit(bobAmount, maxPrice); // End sale as failed vm.warp(endTime); @@ -503,7 +509,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // End sale as failed vm.warp(endTime); @@ -536,7 +542,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); vm.warp(endTime); vm.prank(admin); @@ -571,7 +577,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // End sale (will fail due to not meeting min cap) vm.warp(endTime); @@ -591,7 +597,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); vm.prank(admin); sale.endSale(); @@ -612,7 +618,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); vm.warp(endTime); vm.prank(admin); @@ -644,7 +650,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); vm.warp(endTime); vm.prank(admin); @@ -672,7 +678,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); vm.warp(endTime); vm.prank(admin); @@ -709,7 +715,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); vm.warp(endTime); vm.prank(admin); @@ -747,7 +753,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MIN_CAP); vm.prank(alice); - sale.deposit(MIN_CAP); + sale.deposit(MIN_CAP, maxPrice); vm.prank(admin); sale.endSale(); @@ -782,7 +788,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); vm.prank(admin); sale.endSale(); @@ -802,7 +808,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), MIN_CAP); vm.prank(alice); - sale.deposit(MIN_CAP); + sale.deposit(MIN_CAP, maxPrice); vm.prank(admin); sale.endSale(); @@ -823,7 +829,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MIN_CAP); vm.prank(alice); - sale.deposit(MIN_CAP); + sale.deposit(MIN_CAP, maxPrice); vm.prank(admin); sale.endSale(); @@ -849,7 +855,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), MIN_CAP); vm.prank(alice); - sale.deposit(MIN_CAP); + sale.deposit(MIN_CAP, maxPrice); vm.prank(admin); sale.endSale(); @@ -873,14 +879,14 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), depositAmount); vm.prank(alice); - sale.deposit(depositAmount); + sale.deposit(depositAmount, maxPrice); vm.prank(admin); sale.endSale(); // Check initial balances uint256 initialTreasuryBalance = usdc.balanceOf(TREASURY); - uint256 initialSaleBalance = usdc.balanceOf(address(sale)); + usdc.balanceOf(address(sale)); // Withdraw proceeds vm.prank(admin); @@ -901,7 +907,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), depositAmount); vm.prank(alice); - sale.deposit(depositAmount); + sale.deposit(depositAmount, maxPrice); // Try to withdraw before ending sale vm.prank(admin); @@ -918,7 +924,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), depositAmount); vm.prank(alice); - sale.deposit(depositAmount); + sale.deposit(depositAmount, maxPrice); vm.prank(admin); sale.endSale(); @@ -938,7 +944,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), depositAmount); vm.prank(alice); - sale.deposit(depositAmount); + sale.deposit(depositAmount, maxPrice); vm.prank(admin); sale.endSale(); @@ -962,14 +968,14 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), aliceAmount); vm.prank(alice); - sale.deposit(aliceAmount); + sale.deposit(aliceAmount, maxPrice); // Bob's deposit usdc.mint(bob, bobAmount); vm.prank(bob); usdc.approve(address(sale), bobAmount); vm.prank(bob); - sale.deposit(bobAmount); + sale.deposit(bobAmount, maxPrice); vm.prank(admin); sale.endSale(); @@ -995,7 +1001,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), depositAmount); vm.prank(alice); - sale.deposit(depositAmount); + sale.deposit(depositAmount, maxPrice); vm.prank(admin); sale.endSale(); @@ -1022,7 +1028,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), depositAmount); vm.prank(alice); - sale.deposit(depositAmount); + sale.deposit(depositAmount, maxPrice); vm.prank(admin); sale.endSale(); @@ -1037,6 +1043,130 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(usdc.balanceOf(address(sale)), 0); } + /* ============ updateMaxPrice ============ */ + + function testUpdateMaxPrice_Timing() public { + // Should fail before sale starts + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, block.timestamp, startTime)); + vm.prank(alice); + sale.updateMaxPrice(1e6); + + // Should work during sale + vm.warp(startTime + 1); + vm.prank(alice); + sale.updateMaxPrice(1e6); + assertEq(sale.maxPrices(alice), 1e6); + + // Should fail after sale ends + vm.prank(admin); + sale.endSale(); + + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleAlreadyEnded.selector, block.timestamp)); + vm.prank(alice); + sale.updateMaxPrice(2e6); + } + + function testUpdateMaxPrice_StateChanges() public { + vm.warp(startTime + 1); + + // Initial state + assertEq(sale.maxPrices(alice), 0); + + // First update + uint256 firstPrice = 1e6; + vm.prank(alice); + sale.updateMaxPrice(firstPrice); + assertEq(sale.maxPrices(alice), firstPrice); + + // Update to higher price + uint256 higherPrice = 2e6; + vm.prank(alice); + sale.updateMaxPrice(higherPrice); + assertEq(sale.maxPrices(alice), higherPrice); + + // Update to lower price + uint256 lowerPrice = 5e5; + vm.prank(alice); + sale.updateMaxPrice(lowerPrice); + assertEq(sale.maxPrices(alice), lowerPrice); + + // Update to same price + vm.prank(alice); + sale.updateMaxPrice(lowerPrice); + assertEq(sale.maxPrices(alice), lowerPrice); + + // Update to zero + vm.prank(alice); + sale.updateMaxPrice(0); + assertEq(sale.maxPrices(alice), 0); + + // Update to max uint256 + vm.prank(alice); + sale.updateMaxPrice(type(uint256).max); + assertEq(sale.maxPrices(alice), type(uint256).max); + } + + function testUpdateMaxPrice_MultipleUsersIndependently() public { + vm.warp(startTime + 1); + + // Update prices for different users + vm.prank(alice); + sale.updateMaxPrice(1e6); + assertEq(sale.maxPrices(alice), 1e6); + + vm.prank(bob); + sale.updateMaxPrice(2e6); + assertEq(sale.maxPrices(bob), 2e6); + + // Verify changes don't affect other users + assertEq(sale.maxPrices(alice), 1e6); + assertEq(sale.maxPrices(bob), 2e6); + + // Update alice's price again + vm.prank(alice); + sale.updateMaxPrice(3e6); + assertEq(sale.maxPrices(alice), 3e6); + assertEq(sale.maxPrices(bob), 2e6); + } + + function testUpdateMaxPrice_WithDeposit() public { + vm.warp(startTime + 1); + + // Setup initial deposit with maxPrice + uint256 depositAmount = 1000 * 1e6; + uint256 initialMaxPrice = 2e6; + + usdc.mint(alice, depositAmount); + vm.prank(alice); + usdc.approve(address(sale), depositAmount); + + vm.prank(alice); + sale.deposit(depositAmount, initialMaxPrice); + assertEq(sale.maxPrices(alice), initialMaxPrice); + + // Update maxPrice after deposit + uint256 newMaxPrice = 3e6; + vm.prank(alice); + sale.updateMaxPrice(newMaxPrice); + assertEq(sale.maxPrices(alice), newMaxPrice); + + // Verify deposit amount remains unchanged + assertEq(sale.deposits(alice), depositAmount); + } + + function testUpdateMaxPrice_EventEmission() public { + vm.warp(startTime + 1); + + uint256 oldPrice = 0; // Initial price + uint256 newPrice = 1e6; + + vm.expectEmit(true, false, false, true); + emit SealedBidTokenSale.MaxPriceUpdated(alice, oldPrice, newPrice); + + vm.prank(alice); + sale.updateMaxPrice(newPrice); + } + /* ============ saleStatus ============ */ function testSaleStatus_InitialState() public view { @@ -1054,6 +1184,7 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(info.hasClaimed, false, "User should not have claimed"); assertEq(info.depositAmount, 0, "User deposit amount should be 0"); assertEq(info.contributorCount, 0, "Contributor count should be 0"); + assertEq(info.maxPrice, 0, "maxPrice should be 0"); } function testSaleStatus_AfterDeposit() public { @@ -1067,7 +1198,7 @@ contract SealedBidTokenSaleTest is SharedSetup { // Make deposit vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // Check state after deposit SealedBidTokenSale.SaleInfo memory info = sale.saleStatus(alice); @@ -1087,7 +1218,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // End sale without reaching cap vm.warp(endTime); @@ -1104,6 +1235,7 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(info.totalWithdrawn, amount, "Total withdrawn should match deposit"); assertEq(info.depositAmount, 0, "User deposit should be 0 after withdrawal"); assertEq(info.saleEnded, true, "Sale should be ended"); + assertEq(info.maxPrice, maxPrice, "maxPrice should be maxPrice"); } function testSaleStatus_AfterSuccessfulSaleAndClaim() public { @@ -1117,7 +1249,7 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc.approve(address(sale), MAX_CAP); vm.prank(alice); - sale.deposit(MAX_CAP); + sale.deposit(MAX_CAP, maxPrice); // End sale vm.warp(endTime); @@ -1138,6 +1270,7 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(info.saleEnded, true, "Sale should be ended"); assertEq(info.capReached, true, "Cap should be reached"); assertEq(info.hasClaimed, true, "User should have claimed"); + assertEq(info.maxPrice, maxPrice, "maxPrice should be maxPrice"); } function testSaleStatus_MultipleUsers() public { @@ -1150,14 +1283,14 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); usdc.approve(address(sale), amount); vm.prank(alice); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // Setup Bob usdc.mint(bob, amount); vm.prank(bob); usdc.approve(address(sale), amount); vm.prank(bob); - sale.deposit(amount); + sale.deposit(amount, maxPrice); // Check states for both users SealedBidTokenSale.SaleInfo memory aliceInfo = sale.saleStatus(alice); From b266e4680a80dc809354999b29fb6b8ed7fa6778 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Thu, 6 Feb 2025 10:08:50 -0600 Subject: [PATCH 24/28] Refactor deposit logic to enforce a minimum deposit amount and update corresponding tests. --- src/apps/SealedBidTokenSale.sol | 13 +++++++++---- test/unit/apps/SealedBidTokenSale.t.sol | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 24dc450a..e87f3ef5 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -53,8 +53,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error CapNotReached(); /// @notice Thrown when attempting withdrawals if cap is reached error CapReached(); - /// @notice Thrown when attempting to deposit zero amount - error ZeroDeposit(); /// @notice Thrown when user has no funds to withdraw error NothingToWithdraw(address user); /// @notice Thrown when attempting to claim tokens more than once @@ -63,6 +61,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error InvalidProof(bytes32[] proof, bytes32 leaf); /// @notice Thrown when attempting claims before Merkle root is set error MerkleRootNotSet(); + /// @notice Thrown when attempting to deposit less than MIN_DEPOSIT + error MinDeposit(uint256 amount); /* ============ Events ============ */ @@ -97,7 +97,12 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /// @param newPrice New max price value event MaxPriceUpdated(address indexed user, uint256 oldPrice, uint256 newPrice); - /* ============ Immutable Parameters ============ */ + /* ============ Constant ============ */ + + /// @notice Token being sold in the sale + uint256 public constant MIN_DEPOSIT = 250 * 1e6; + + /* ============ Immutable ============ */ /// @notice Token being sold in the sale IERC20 public immutable saleToken; @@ -173,7 +178,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { // Verify sale is active and deposit is valid if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); if (saleEnded) revert SaleAlreadyEnded(block.timestamp); - if (amount == 0) revert ZeroDeposit(); + if (amount < MIN_DEPOSIT) revert MinDeposit(amount); // Update deposit accounting deposits[msg.sender] += amount; diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index e108fa8a..9b42730b 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -179,21 +179,21 @@ contract SealedBidTokenSaleTest is SharedSetup { sale.deposit(amount, maxPrice); } - function testDeposit_RevertWhen_ZeroAmount() public { + function testDeposit_RevertWhen_MinAmount() public { // Advance time to start of sale vm.warp(startTime + 1); // Try to deposit zero amount vm.prank(alice); - vm.expectRevert(SealedBidTokenSale.ZeroDeposit.selector); - sale.deposit(0, maxPrice); + vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.MinDeposit.selector, 250 * 1e6 - 1)); + sale.deposit(250 * 1e6 - 1, maxPrice); } function testDeposit_MultipleDeposits() public { // Advance time to start of sale vm.warp(startTime + 1); - uint256 firstAmount = 1000 * 1e6; + uint256 firstAmount = 250 * 1e6; uint256 secondAmount = 2000 * 1e6; uint256 totalAmount = firstAmount + secondAmount; From aef2a44aaedf56e4583b6c3ccd126177c1c3d5c4 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Thu, 6 Feb 2025 13:30:19 -0600 Subject: [PATCH 25/28] Add max price range check in SealedBidTokenSale and update unit tests accordingly. --- src/apps/SealedBidTokenSale.sol | 10 ++++++ test/unit/apps/SealedBidTokenSale.t.sol | 46 ++++++++++--------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index e87f3ef5..32aceaec 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -63,6 +63,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error MerkleRootNotSet(); /// @notice Thrown when attempting to deposit less than MIN_DEPOSIT error MinDeposit(uint256 amount); + /// @notice Thrown when new max price is out of range + error MaxPriceOutOfRange(uint256 amount); /* ============ Events ============ */ @@ -179,6 +181,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); if (saleEnded) revert SaleAlreadyEnded(block.timestamp); if (amount < MIN_DEPOSIT) revert MinDeposit(amount); + _checkMaxPrice(maxPrice); // Update deposit accounting deposits[msg.sender] += amount; @@ -272,12 +275,19 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { function updateMaxPrice(uint256 newMaxPrice) external nonReentrant { if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); if (saleEnded) revert SaleAlreadyEnded(block.timestamp); + _checkMaxPrice(newMaxPrice); uint256 oldPrice = maxPrices[msg.sender]; maxPrices[msg.sender] = newMaxPrice; emit MaxPriceUpdated(msg.sender, oldPrice, newMaxPrice); } + function _checkMaxPrice(uint256 newMaxPrice) internal pure { + if (newMaxPrice < 10 * 1e6 || newMaxPrice > 30 * 1e6) { + revert MaxPriceOutOfRange(newMaxPrice); + } + } + /* ============ Admin Functions ============ */ /** diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 9b42730b..f3048457 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -24,7 +24,7 @@ contract SealedBidTokenSaleTest is SharedSetup { bytes32[] public proof; uint256 public saleTokenAllocation = 1000 * 1e18; uint256 public usdcAllocation = 1000 * 1e6; - uint256 public maxPrice = 1000 * 1e6; + uint256 public maxPrice = 10 * 1e6; function setUp() public override { super.setUp(); @@ -1054,8 +1054,8 @@ contract SealedBidTokenSaleTest is SharedSetup { // Should work during sale vm.warp(startTime + 1); vm.prank(alice); - sale.updateMaxPrice(1e6); - assertEq(sale.maxPrices(alice), 1e6); + sale.updateMaxPrice(10e6); + assertEq(sale.maxPrices(alice), 10e6); // Should fail after sale ends vm.prank(admin); @@ -1073,19 +1073,19 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(sale.maxPrices(alice), 0); // First update - uint256 firstPrice = 1e6; + uint256 firstPrice = 10e6; vm.prank(alice); sale.updateMaxPrice(firstPrice); assertEq(sale.maxPrices(alice), firstPrice); // Update to higher price - uint256 higherPrice = 2e6; + uint256 higherPrice = 20e6; vm.prank(alice); sale.updateMaxPrice(higherPrice); assertEq(sale.maxPrices(alice), higherPrice); // Update to lower price - uint256 lowerPrice = 5e5; + uint256 lowerPrice = 15e6; vm.prank(alice); sale.updateMaxPrice(lowerPrice); assertEq(sale.maxPrices(alice), lowerPrice); @@ -1094,16 +1094,6 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.prank(alice); sale.updateMaxPrice(lowerPrice); assertEq(sale.maxPrices(alice), lowerPrice); - - // Update to zero - vm.prank(alice); - sale.updateMaxPrice(0); - assertEq(sale.maxPrices(alice), 0); - - // Update to max uint256 - vm.prank(alice); - sale.updateMaxPrice(type(uint256).max); - assertEq(sale.maxPrices(alice), type(uint256).max); } function testUpdateMaxPrice_MultipleUsersIndependently() public { @@ -1111,22 +1101,22 @@ contract SealedBidTokenSaleTest is SharedSetup { // Update prices for different users vm.prank(alice); - sale.updateMaxPrice(1e6); - assertEq(sale.maxPrices(alice), 1e6); + sale.updateMaxPrice(10e6); + assertEq(sale.maxPrices(alice), 10e6); vm.prank(bob); - sale.updateMaxPrice(2e6); - assertEq(sale.maxPrices(bob), 2e6); + sale.updateMaxPrice(20e6); + assertEq(sale.maxPrices(bob), 20e6); // Verify changes don't affect other users - assertEq(sale.maxPrices(alice), 1e6); - assertEq(sale.maxPrices(bob), 2e6); + assertEq(sale.maxPrices(alice), 10e6); + assertEq(sale.maxPrices(bob), 20e6); // Update alice's price again vm.prank(alice); - sale.updateMaxPrice(3e6); - assertEq(sale.maxPrices(alice), 3e6); - assertEq(sale.maxPrices(bob), 2e6); + sale.updateMaxPrice(30e6); + assertEq(sale.maxPrices(alice), 30e6); + assertEq(sale.maxPrices(bob), 20e6); } function testUpdateMaxPrice_WithDeposit() public { @@ -1134,7 +1124,7 @@ contract SealedBidTokenSaleTest is SharedSetup { // Setup initial deposit with maxPrice uint256 depositAmount = 1000 * 1e6; - uint256 initialMaxPrice = 2e6; + uint256 initialMaxPrice = 10 * 1e6; usdc.mint(alice, depositAmount); vm.prank(alice); @@ -1145,7 +1135,7 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(sale.maxPrices(alice), initialMaxPrice); // Update maxPrice after deposit - uint256 newMaxPrice = 3e6; + uint256 newMaxPrice = 30 * 1e6; vm.prank(alice); sale.updateMaxPrice(newMaxPrice); assertEq(sale.maxPrices(alice), newMaxPrice); @@ -1158,7 +1148,7 @@ contract SealedBidTokenSaleTest is SharedSetup { vm.warp(startTime + 1); uint256 oldPrice = 0; // Initial price - uint256 newPrice = 1e6; + uint256 newPrice = 10e6; vm.expectEmit(true, false, false, true); emit SealedBidTokenSale.MaxPriceUpdated(alice, oldPrice, newPrice); From bb32eedb9e82c123a822c96caee55c9ec34fd236 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Thu, 6 Feb 2025 16:31:00 -0600 Subject: [PATCH 26/28] Add early participation window for emissaries in SealedBidTokenSale contract with tests for deposit logic and boundary conditions. --- src/apps/SealedBidTokenSale.sol | 65 +++++++-- test/unit/apps/SealedBidTokenSale.t.sol | 177 +++++++++++++++++++++++- 2 files changed, 227 insertions(+), 15 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 32aceaec..0a10ab44 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -16,6 +16,7 @@ import {SafeERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/utils/SafeERC * - USDC deposits from users * - Merkle-based token allocation claims * - Full refunds if minimum cap not reached + * - Early participation window for first 700 emissaries */ contract SealedBidTokenSale is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; @@ -23,17 +24,33 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /* ============ Struct ============ */ struct SaleInfo { + /// @notice Timestamp when emissary early access begins + uint256 preStartTime; + /// @notice Timestamp when public sale begins uint256 startTime; + /// @notice Minimum USDC required for sale success uint256 minimumCap; + /// @notice Total USDC deposited by all users uint256 totalDeposited; + /// @notice Total USDC withdrawn after failed sale uint256 totalWithdrawn; + /// @notice Total USDC claimed by users uint256 totalUsdcClaimed; + /// @notice Total sale tokens claimed uint256 totalSaleTokenClaimed; + /// @notice Whether sale has been officially ended bool saleEnded; + /// @notice Whether minimum cap was reached bool capReached; + /// @notice Whether specified user has claimed tokens bool hasClaimed; + /// @notice Total number of unique depositors uint256 contributorCount; + /// @notice Current number of emissary participants + uint256 currentEmissaryCount; + /// @notice Deposit amount for specified user uint256 depositAmount; + /// @notice Max price set by specified user uint256 maxPrice; } @@ -65,6 +82,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { error MinDeposit(uint256 amount); /// @notice Thrown when new max price is out of range error MaxPriceOutOfRange(uint256 amount); + /// @notice Thrown when emissary slots are fully occupied + error EmissaryFull(); + /// @notice Thrown when time configuration is invalid + error InvalidTimeConfiguration(); /* ============ Events ============ */ @@ -103,6 +124,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { /// @notice Token being sold in the sale uint256 public constant MIN_DEPOSIT = 250 * 1e6; + /// @notice Maximum number of emissaries + uint256 public constant MAX_EMISSARIES = 700; /* ============ Immutable ============ */ @@ -112,6 +135,8 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { IERC20 public immutable USDC; /// @notice Address where sale proceeds will be sent address public immutable treasury; + /// @notice Timestamp when emissary early access begins + uint256 public immutable preStartTime; /// @notice Timestamp when the sale begins uint256 public immutable startTime; /// @notice Minimum amount of USDC required for sale success @@ -141,6 +166,10 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { mapping(address => uint256) public maxPrices; /// @notice Count of all contributors uint256 public contributorCount; + /// @notice Current number of emissary participants + uint256 public currentEmissaryCount; + /// @notice Maps user addresses to emissary status + mapping(address => bool) public isEmissary; /* ============ Constructor ============ */ @@ -152,15 +181,22 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param _startTime Timestamp when sale will begin * @param _minimumCap Minimum USDC amount required for sale success */ - constructor(address _saleToken, address _treasury, address _usdcToken, uint256 _startTime, uint256 _minimumCap) - Ownable(msg.sender) - { + constructor( + address _saleToken, + address _treasury, + address _usdcToken, + uint256 _preStartTime, + uint256 _startTime, + uint256 _minimumCap + ) Ownable(msg.sender) { if (_saleToken == address(0)) revert InvalidSaleTokenAddress(_saleToken); if (_treasury == address(0)) revert InvalidTreasuryAddress(_treasury); + if (_preStartTime >= _startTime) revert InvalidTimeConfiguration(); saleToken = IERC20(_saleToken); treasury = _treasury; USDC = IERC20(_usdcToken); + preStartTime = _preStartTime; startTime = _startTime; minimumCap = _minimumCap; } @@ -177,13 +213,20 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param maxPrice The maximum price set by the user for the token sale */ function deposit(uint256 amount, uint256 maxPrice) external nonReentrant { - // Verify sale is active and deposit is valid - if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); if (saleEnded) revert SaleAlreadyEnded(block.timestamp); + if (block.timestamp < preStartTime) revert SaleNotStarted(block.timestamp, preStartTime); if (amount < MIN_DEPOSIT) revert MinDeposit(amount); _checkMaxPrice(maxPrice); - // Update deposit accounting + // Handle emissary period + if (block.timestamp < startTime) { + if (currentEmissaryCount >= MAX_EMISSARIES) revert EmissaryFull(); + if (!isEmissary[msg.sender]) { + isEmissary[msg.sender] = true; + currentEmissaryCount++; + } + } + deposits[msg.sender] += amount; totalDeposited += amount; contributorCount++; @@ -273,7 +316,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { * @param newMaxPrice The new maximum price value to be set for the user. */ function updateMaxPrice(uint256 newMaxPrice) external nonReentrant { - if (block.timestamp < startTime) revert SaleNotStarted(block.timestamp, startTime); + if (block.timestamp < preStartTime) revert SaleNotStarted(block.timestamp, preStartTime); if (saleEnded) revert SaleAlreadyEnded(block.timestamp); _checkMaxPrice(newMaxPrice); @@ -283,9 +326,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { } function _checkMaxPrice(uint256 newMaxPrice) internal pure { - if (newMaxPrice < 10 * 1e6 || newMaxPrice > 30 * 1e6) { - revert MaxPriceOutOfRange(newMaxPrice); - } + if (newMaxPrice < 10 * 1e6 || newMaxPrice > 30 * 1e6) revert MaxPriceOutOfRange(newMaxPrice); } /* ============ Admin Functions ============ */ @@ -342,6 +383,7 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { function saleStatus(address user) external view returns (SaleInfo memory) { return SaleInfo({ + preStartTime: preStartTime, startTime: startTime, minimumCap: minimumCap, totalDeposited: totalDeposited, @@ -351,8 +393,9 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { saleEnded: saleEnded, capReached: capReached, hasClaimed: hasClaimed[user], - depositAmount: deposits[user], contributorCount: contributorCount, + currentEmissaryCount: currentEmissaryCount, + depositAmount: deposits[user], maxPrice: maxPrices[user] }); } diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index f3048457..8f54a873 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -15,6 +15,7 @@ contract SealedBidTokenSaleTest is SharedSetup { ERC20Mock public usdc; ERC20Mock public saleToken; + uint256 public preStartTime; uint256 public startTime; uint256 public endTime; uint256 public constant MIN_CAP = 10e6 * 1e6; @@ -29,7 +30,8 @@ contract SealedBidTokenSaleTest is SharedSetup { function setUp() public override { super.setUp(); - startTime = block.timestamp + 1 days; + preStartTime = block.timestamp + 1 days; + startTime = block.timestamp + 2 days; endTime = startTime + 4 days; // Deploy mock tokens @@ -38,7 +40,7 @@ contract SealedBidTokenSaleTest is SharedSetup { // Deploy sale contract with admin as owner vm.prank(admin); - sale = new SealedBidTokenSale(address(saleToken), TREASURY, address(usdc), startTime, MIN_CAP); + sale = new SealedBidTokenSale(address(saleToken), TREASURY, address(usdc), preStartTime, startTime, MIN_CAP); // Setup Merkle tree with alice and bob bytes32[] memory leaves = new bytes32[](2); @@ -155,7 +157,9 @@ contract SealedBidTokenSaleTest is SharedSetup { } function testDeposit_RevertWhen_BeforeStart() public { - vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, block.timestamp, startTime)); + vm.expectRevert( + abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, block.timestamp, preStartTime) + ); vm.prank(alice); sale.deposit(100 ether, maxPrice); } @@ -1047,7 +1051,9 @@ contract SealedBidTokenSaleTest is SharedSetup { function testUpdateMaxPrice_Timing() public { // Should fail before sale starts - vm.expectRevert(abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, block.timestamp, startTime)); + vm.expectRevert( + abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, block.timestamp, preStartTime) + ); vm.prank(alice); sale.updateMaxPrice(1e6); @@ -1293,4 +1299,167 @@ contract SealedBidTokenSaleTest is SharedSetup { assertEq(aliceInfo.contributorCount, 2, "Contributor count should be 2"); assertEq(bobInfo.contributorCount, 2, "Contributor count should be same for all users"); } + + /* ============ saleStatus ============ */ + + function testEmissaryDeposit_During_EarlyAccess() public { + // Set time to early access period + vm.warp(preStartTime + 1); + + uint256 amount = 1000 * 1e6; + uint256 initialEmissaryCount = sale.currentEmissaryCount(); + + // Setup deposit + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + // Make deposit during early access + vm.prank(alice); + sale.deposit(amount, maxPrice); + + // Verify emissary status + assertTrue(sale.isEmissary(alice)); + assertEq(sale.currentEmissaryCount(), initialEmissaryCount + 1); + assertEq(sale.deposits(alice), amount); + } + + function testEmissaryDeposit_RevertWhen_MaxEmissariesReached() public { + // Set time to early access period + vm.warp(preStartTime + 1); + + uint256 amount = 1000 * 1e6; + + // Fill up emissary slots + for (uint256 i = 0; i < sale.MAX_EMISSARIES(); i++) { + address emissary = address(uint160(i + 1000)); // Generate unique addresses + + usdc.mint(emissary, amount); + vm.prank(emissary); + usdc.approve(address(sale), amount); + + vm.prank(emissary); + sale.deposit(amount, maxPrice); + } + + // Try to add one more emissary + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.expectRevert(SealedBidTokenSale.EmissaryFull.selector); + vm.prank(alice); + sale.deposit(amount, maxPrice); + } + + function testEmissaryDeposit_MultipleDeposits_SameEmissary() public { + // Set time to early access period + vm.warp(preStartTime + 1); + + uint256 amount = 1000 * 1e6; + uint256 initialEmissaryCount = sale.currentEmissaryCount(); + + // First deposit + usdc.mint(alice, amount * 2); + vm.prank(alice); + usdc.approve(address(sale), amount * 2); + + vm.prank(alice); + sale.deposit(amount, maxPrice); + + // Second deposit from same emissary + vm.prank(alice); + sale.deposit(amount, maxPrice); + + // Verify emissary count only increased once + assertTrue(sale.isEmissary(alice)); + assertEq(sale.currentEmissaryCount(), initialEmissaryCount + 1); + assertEq(sale.deposits(alice), amount * 2); + } + + function testDeposit_After_EmissaryPeriod() public { + // Set time after early access period + vm.warp(startTime + 1); + + uint256 amount = 1000 * 1e6; + + // Setup deposit + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + // Make regular deposit after early access + vm.prank(alice); + sale.deposit(amount, maxPrice); + + // Verify not counted as emissary + assertFalse(sale.isEmissary(alice)); + assertEq(sale.currentEmissaryCount(), 0); + assertEq(sale.deposits(alice), amount); + } + + function testSaleStatus_EmissaryCount() public { + // Set time to early access period + vm.warp(preStartTime + 1); + + uint256 amount = 1000 * 1e6; + + // Add a few emissaries + for (uint256 i = 0; i < 3; i++) { + address emissary = address(uint160(i + 1000)); + + usdc.mint(emissary, amount); + vm.prank(emissary); + usdc.approve(address(sale), amount); + + vm.prank(emissary); + sale.deposit(amount, maxPrice); + } + + // Check emissary count in status + SealedBidTokenSale.SaleInfo memory info = sale.saleStatus(alice); + assertEq(info.currentEmissaryCount, 3); + } + + function testEmissaryDeposit_Boundaries() public { + uint256 amount = 1000 * 1e6; + + // Try just before preStartTime + vm.warp(preStartTime - 1); + usdc.mint(alice, amount); + vm.prank(alice); + usdc.approve(address(sale), amount); + + vm.expectRevert( + abi.encodeWithSelector(SealedBidTokenSale.SaleNotStarted.selector, preStartTime - 1, preStartTime) + ); + vm.prank(alice); + sale.deposit(amount, maxPrice); + + // Try at exactly preStartTime + vm.warp(preStartTime); + vm.prank(alice); + sale.deposit(amount, maxPrice); + assertTrue(sale.isEmissary(alice)); + + // Try just before startTime + vm.warp(startTime - 1); + usdc.mint(bob, amount); + vm.prank(bob); + usdc.approve(address(sale), amount); + + vm.prank(bob); + sale.deposit(amount, maxPrice); + assertTrue(sale.isEmissary(bob)); + + // Try at exactly startTime + vm.warp(startTime); + usdc.mint(address(0x123), amount); + vm.prank(address(0x123)); + usdc.approve(address(sale), amount); + + vm.prank(address(0x123)); + sale.deposit(amount, maxPrice); + assertFalse(sale.isEmissary(address(0x123))); + } } From 6bfa48efb258678b2e03c3b8b033ed7231f398dd Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Fri, 7 Feb 2025 18:17:40 -0600 Subject: [PATCH 27/28] Refactor SealedBidTokenSale contract to support UUPS upgradeability, update imports, and adjust unit tests for proxy initialization. --- src/apps/SealedBidTokenSale.sol | 29 ++++++++++++++++++++----- test/unit/apps/SealedBidTokenSale.t.sol | 7 ++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/apps/SealedBidTokenSale.sol b/src/apps/SealedBidTokenSale.sol index 0a10ab44..19b4ae11 100644 --- a/src/apps/SealedBidTokenSale.sol +++ b/src/apps/SealedBidTokenSale.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {Ownable} from "@openzeppelin-5.0.1/contracts/access/Ownable.sol"; +import {OwnableUpgradeable} from "@openzeppelin-5.0.1/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin-5.0.1/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ReentrancyGuardUpgradeable} from + "@openzeppelin-5.0.1/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin-5.0.1/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {IERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/IERC20.sol"; -import {ReentrancyGuard} from "@openzeppelin-5.0.1/contracts/utils/ReentrancyGuard.sol"; import {MerkleProof} from "@openzeppelin-5.0.1/contracts/utils/cryptography/MerkleProof.sol"; import {SafeERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -18,7 +21,7 @@ import {SafeERC20} from "@openzeppelin-5.0.1/contracts/token/ERC20/utils/SafeERC * - Full refunds if minimum cap not reached * - Early participation window for first 700 emissaries */ -contract SealedBidTokenSale is Ownable, ReentrancyGuard { +contract SealedBidTokenSale is Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; /* ============ Struct ============ */ @@ -172,7 +175,6 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { mapping(address => bool) public isEmissary; /* ============ Constructor ============ */ - /** * @notice Initializes the token sale with required parameters * @param _saleToken Address of the token being sold @@ -188,7 +190,9 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { uint256 _preStartTime, uint256 _startTime, uint256 _minimumCap - ) Ownable(msg.sender) { + ) { + _disableInitializers(); + if (_saleToken == address(0)) revert InvalidSaleTokenAddress(_saleToken); if (_treasury == address(0)) revert InvalidTreasuryAddress(_treasury); if (_preStartTime >= _startTime) revert InvalidTimeConfiguration(); @@ -201,6 +205,21 @@ contract SealedBidTokenSale is Ownable, ReentrancyGuard { minimumCap = _minimumCap; } + /// @dev initialize the proxy + function initialize() external virtual initializer { + __Ownable_init(msg.sender); + __UUPSUpgradeable_init(); + } + + /** + * @dev Authorize the upgrade. Only by an owner. + * @param newImplementation address of the new implementation + */ + // This function is called by the proxy contract when the factory is upgraded + function _authorizeUpgrade(address newImplementation) internal view override onlyOwner { + (newImplementation); + } + /* ============ User Functions ============ */ /** diff --git a/test/unit/apps/SealedBidTokenSale.t.sol b/test/unit/apps/SealedBidTokenSale.t.sol index 8f54a873..30294359 100644 --- a/test/unit/apps/SealedBidTokenSale.t.sol +++ b/test/unit/apps/SealedBidTokenSale.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import {Ownable} from "@openzeppelin-5.0.1/contracts/access/Ownable.sol"; +import {UUPSProxy} from "@kinto-core-test/helpers/UUPSProxy.sol"; import {Test} from "forge-std/Test.sol"; import {SealedBidTokenSale} from "@kinto-core/apps/SealedBidTokenSale.sol"; import {ERC20Mock} from "@kinto-core-test/helpers/ERC20Mock.sol"; @@ -38,9 +39,11 @@ contract SealedBidTokenSaleTest is SharedSetup { usdc = new ERC20Mock("USDC", "USDC", 6); saleToken = new ERC20Mock("K", "KINTO", 18); - // Deploy sale contract with admin as owner - vm.prank(admin); sale = new SealedBidTokenSale(address(saleToken), TREASURY, address(usdc), preStartTime, startTime, MIN_CAP); + vm.startPrank(admin); + sale = SealedBidTokenSale(address(new UUPSProxy{salt: 0}(address(sale), ""))); + sale.initialize(); + vm.stopPrank(); // Setup Merkle tree with alice and bob bytes32[] memory leaves = new bytes32[](2); From f135245c149c9cdee6c4fef033e74f29af426538 Mon Sep 17 00:00:00 2001 From: Igor Yalovoy Date: Sat, 8 Feb 2025 10:13:23 -0600 Subject: [PATCH 28/28] Add SealedBidTokenSale deployment script, update artifacts with new contract addresses, and include transaction details in JSON files for chain 7887. --- .../7887/run-1739030918.json | 302 ++++++++++++++++++ .../7887/run-latest.json | 302 ++++++++++++++++++ script/migrations/155-deploy-sale.s.sol | 71 ++++ test/artifacts/7887/addresses.json | 4 +- 4 files changed, 678 insertions(+), 1 deletion(-) create mode 100644 broadcast/155-deploy-sale.s.sol/7887/run-1739030918.json create mode 100644 broadcast/155-deploy-sale.s.sol/7887/run-latest.json create mode 100644 script/migrations/155-deploy-sale.s.sol diff --git a/broadcast/155-deploy-sale.s.sol/7887/run-1739030918.json b/broadcast/155-deploy-sale.s.sol/7887/run-1739030918.json new file mode 100644 index 00000000..1f85eff9 --- /dev/null +++ b/broadcast/155-deploy-sale.s.sol/7887/run-1739030918.json @@ -0,0 +1,302 @@ +{ + "transactions": [ + { + "hash": "0x55f9c9c447311fbe329d2f01414445cdfff118ffe049dd1127a20dedbfaa1f3e", + "transactionType": "CREATE", + "contractName": "SealedBidTokenSale", + "contractAddress": "0xe9ab275d7e9859bbef11a79b7c1854a030d0c171", + "function": null, + "arguments": [ + "0x010700808D59d2bb92257fCafACfe8e5bFF7aB87", + "0x793500709506652Fcc61F0d2D0fDa605638D4293", + "0x05DC0010C9902EcF6CBc921c6A4bd971c69E5A2E", + "1739296800", + "1739901600", + "250000000000" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "value": "0x0", + "input": "0x6101606040523060805234801562000015575f80fd5b50604051620021bd380380620021bd8339810160408190526200003891620001d2565b6200004262000102565b6001600160a01b0386166200007a576040516307094c3360e11b81526001600160a01b03871660048201526024015b60405180910390fd5b6001600160a01b038516620000ae576040516334d5d27d60e21b81526001600160a01b038616600482015260240162000071565b818310620000cf57604051631800812760e11b815260040160405180910390fd5b6001600160a01b0395861660a05293851660e0529190931660c05261010092909252610120919091526101405262000234565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff1615620001535760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b0390811614620001b35780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b80516001600160a01b0381168114620001cd575f80fd5b919050565b5f805f805f8060c08789031215620001e8575f80fd5b620001f387620001b6565b95506200020360208801620001b6565b94506200021360408801620001b6565b9350606087015192506080870151915060a087015190509295509295509295565b60805160a05160c05160e051610100516101205161014051611eb9620003045f395f8181610689015281816109a90152610d6601525f818161048401528181610d3c01526111a301525f81816102ec01528181610b6801528181610ba301528181610d1901528181611109015261114401525f8181610388015261101601525f81816104ff015281816108d501528181610ae901528181611040015281816110b3015261129601525f8181610622015261088401525f8181611498015281816114c101526116000152611eb95ff3fe608060405260043610610207575f3560e01c806378e9792511610113578063ad3cb1cc1161009d578063ecfd89281161006d578063ecfd892814610644578063f2fde38b14610659578063f381f2a514610678578063fc7e286d146106ab578063ff50abdc146106d6575f80fd5b8063ad3cb1cc1461059e578063e1e158a5146105db578063e2bbb158146105f2578063e985e36714610611575f80fd5b806389a30271116100e357806389a30271146104ee5780638bd29968146105215780638da5cb5b146105365780639038e693146105725780639b8906ae14610586575f80fd5b806378e97925146104735780637cb64759146104a65780638129fc1c146104c557806388de2330146104d9575f80fd5b80634f93594511610194578063665828021161016457806366582802146103c25780636bb8a6e0146103ee578063715018a61461041c578063734d6db31461043057806373b2e80e14610445575f80fd5b80634f9359451461032157806352d1902d1461034e57806358334e291461036257806361d027b314610377575f80fd5b80634602d509116101da5780634602d5091461027c578063469619f2146102a75780634b319713146102c65780634dc41210146102db5780634f1ef2861461030e575f80fd5b80632496903c1461020b5780632eb4a7ab1461022c578063380d831b146102545780633ccfd60b14610268575b5f80fd5b348015610216575f80fd5b5061022a610225366004611ab4565b6106eb565b005b348015610237575f80fd5b5061024160055481565b6040519081526020015b60405180910390f35b34801561025f575f80fd5b5061022a610974565b348015610273575f80fd5b5061022a610a2d565b348015610287575f80fd5b50610241610296366004611b46565b60086020525f908152604090205481565b3480156102b2575f80fd5b5061022a6102c1366004611b5f565b610b5e565b3480156102d1575f80fd5b5061024160015481565b3480156102e6575f80fd5b506102417f000000000000000000000000000000000000000000000000000000000000000081565b61022a61031c366004611b8a565b610c67565b34801561032c575f80fd5b505f5461033e90610100900460ff1681565b604051901515815260200161024b565b348015610359575f80fd5b50610241610c86565b34801561036d575f80fd5b5061024160035481565b348015610382575f80fd5b506103aa7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b03909116815260200161024b565b3480156103cd575f80fd5b506103e16103dc366004611b46565b610ca1565b60405161024b9190611c46565b3480156103f9575f80fd5b5061033e610408366004611b46565b600b6020525f908152604090205460ff1681565b348015610427575f80fd5b5061022a610e21565b34801561043b575f80fd5b5061024160045481565b348015610450575f80fd5b5061033e61045f366004611b46565b60076020525f908152604090205460ff1681565b34801561047e575f80fd5b506102417f000000000000000000000000000000000000000000000000000000000000000081565b3480156104b1575f80fd5b5061022a6104c0366004611b5f565b610e32565b3480156104d0575f80fd5b5061022a610eab565b3480156104e4575f80fd5b50610241600a5481565b3480156104f9575f80fd5b506103aa7f000000000000000000000000000000000000000000000000000000000000000081565b34801561052c575f80fd5b506102416102bc81565b348015610541575f80fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b03166103aa565b34801561057d575f80fd5b5061022a610fbf565b348015610591575f80fd5b505f5461033e9060ff1681565b3480156105a9575f80fd5b506105ce604051806040016040528060058152602001640352e302e360dc1b81525081565b60405161024b9190611d11565b3480156105e6575f80fd5b50610241630ee6b28081565b3480156105fd575f80fd5b5061022a61060c366004611d43565b6110da565b34801561061c575f80fd5b506103aa7f000000000000000000000000000000000000000000000000000000000000000081565b34801561064f575f80fd5b5061024160095481565b348015610664575f80fd5b5061022a610673366004611b46565b611311565b348015610683575f80fd5b506102417f000000000000000000000000000000000000000000000000000000000000000081565b3480156106b6575f80fd5b506102416106c5366004611b46565b60066020525f908152604090205481565b3480156106e1575f80fd5b5061024160025481565b6106f361134b565b5f5460ff16158061070b57505f54610100900460ff16155b156107295760405163b4202dd560e01b815260040160405180910390fd5b60055461074957604051634fc5147960e11b815260040160405180910390fd5b6001600160a01b0381165f9081526007602052604090205460ff161561079257604051632058b6db60e01b81526001600160a01b03821660048201526024015b60405180910390fd5b604080516001600160a01b0383166020820152908101869052606081018590525f9060800160408051601f198184030181528282528051602091820120908301520160405160208183030381529060405280519060200120905061082c8484808060200260200160405190810160405280939291908181526020018383602002808284375f92019190915250506005549150849050611382565b61084f5783838260405163571e214960e11b815260040161078993929190611d63565b6001600160a01b0382165f908152600760205260409020805460ff1916600117905585156108c2576108ab6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168388611399565b8560045f8282546108bc9190611db3565b90915550505b8415610913576108fc6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168387611399565b8460035f82825461090d9190611db3565b90915550505b816001600160a01b03167fd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a8760405161094e91815260200190565b60405180910390a25061096d60015f80516020611e6483398151915255565b5050505050565b61097c6113fd565b5f5460ff16156109a157604051632252c26960e21b8152426004820152602401610789565b5f80546002547f000000000000000000000000000000000000000000000000000000000000000081101561010090810261ffff1990931692909217600117928390556040517f8233a98787b42c8a85877e43d0969d9758e213000c07b8e1db21707dd06d05d193610a2393900460ff1691909115158252602082015260400190565b60405180910390a1565b610a3561134b565b5f5460ff16610a59576040516336f9bbd760e11b8152426004820152602401610789565b5f54610100900460ff1615610a8157604051636bf4c8e960e11b815260040160405180910390fd5b335f9081526006602052604081205490819003610ab357604051636e34ee0b60e11b8152336004820152602401610789565b335f90815260066020526040812081905560018054839290610ad6908490611db3565b90915550610b1090506001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163383611399565b60405181815233907f7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d59060200160405180910390a250610b5c60015f80516020611e6483398151915255565b565b610b6661134b565b7f0000000000000000000000000000000000000000000000000000000000000000421015610bcf5760405163457f873160e01b81524260048201527f00000000000000000000000000000000000000000000000000000000000000006024820152604401610789565b5f5460ff1615610bf457604051632252c26960e21b8152426004820152602401610789565b610bfd81611458565b335f81815260086020908152604091829020805490859055825181815291820185905292917f18332e48c9676c7696287f3a4e7bcd1cb6cad945dd233d5711ab89cf106b673c910160405180910390a250610c6460015f80516020611e6483398151915255565b50565b610c6f61148d565b610c7882611531565b610c828282611539565b5050565b5f610c8f6115f5565b505f80516020611e4483398151915290565b610d0b604051806101c001604052805f81526020015f81526020015f81526020015f81526020015f81526020015f81526020015f81526020015f151581526020015f151581526020015f151581526020015f81526020015f81526020015f81526020015f81525090565b50604080516101c0810182527f000000000000000000000000000000000000000000000000000000000000000081527f00000000000000000000000000000000000000000000000000000000000000006020808301919091527f0000000000000000000000000000000000000000000000000000000000000000828401526002546060830152600154608083015260035460a083015260045460c08301525f805460ff808216151560e08601526101009182900481161515918501919091526001600160a01b0390951680825260078352848220549095161515610120840152600954610140840152600a546101608401528481526006825283812054610180840152938452600890529120546101a082015290565b610e296113fd565b610b5c5f61163e565b610e3a6113fd565b5f5460ff161580610e5257505f54610100900460ff16155b15610e705760405163b4202dd560e01b815260040160405180910390fd5b60058190556040518181527f42cbc405e4dbf1b691e85b9a34b08ecfcf7a9ad9078bf4d645ccfa1fac11c10b9060200160405180910390a150565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f81158015610ef05750825b90505f8267ffffffffffffffff166001148015610f0c5750303b155b905081158015610f1a575080155b15610f385760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff191660011785558315610f6257845460ff60401b1916600160401b1785555b610f6b336116ae565b610f736116bf565b831561096d57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15050505050565b610fc76113fd565b5f5460ff161580610fdf57505f54610100900460ff16155b15610ffd5760405163b4202dd560e01b815260040160405180910390fd5b6040516370a0823160e01b8152306004820152610b5c907f0000000000000000000000000000000000000000000000000000000000000000906001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906370a0823190602401602060405180830381865afa158015611085573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906110a99190611dc6565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611399565b6110e261134b565b5f5460ff161561110757604051632252c26960e21b8152426004820152602401610789565b7f00000000000000000000000000000000000000000000000000000000000000004210156111705760405163457f873160e01b81524260048201527f00000000000000000000000000000000000000000000000000000000000000006024820152604401610789565b630ee6b28082101561119857604051630266752760e51b815260048101839052602401610789565b6111a181611458565b7f0000000000000000000000000000000000000000000000000000000000000000421015611230576102bc600a54106111ed5760405163b64bf0e160e01b815260040160405180910390fd5b335f908152600b602052604090205460ff1661123057335f908152600b60205260408120805460ff19166001179055600a80549161122a83611ddd565b91905055505b335f908152600660205260408120805484929061124e908490611db3565b925050819055508160025f8282546112669190611db3565b909155505060098054905f61127a83611ddd565b9091555050335f8181526008602052604090208290556112c6907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169030856116c7565b60405182815233907f2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c49060200160405180910390a2610c8260015f80516020611e6483398151915255565b6113196113fd565b6001600160a01b03811661134257604051631e4fbdf760e01b81525f6004820152602401610789565b610c648161163e565b5f80516020611e6483398151915280546001190161137c57604051633ee5aeb560e01b815260040160405180910390fd5b60029055565b5f8261138e8584611706565b1490505b9392505050565b6040516001600160a01b038381166024830152604482018390526113f891859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b03838183161783525050505061174a565b505050565b3361142f7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b031614610b5c5760405163118cdaa760e01b8152336004820152602401610789565b6298968081108061146c57506301c9c38081115b15610c64576040516319bfbdbd60e11b815260048101829052602401610789565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061151357507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166115075f80516020611e44833981519152546001600160a01b031690565b6001600160a01b031614155b15610b5c5760405163703e46dd60e11b815260040160405180910390fd5b610c646113fd565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015611593575060408051601f3d908101601f1916820190925261159091810190611dc6565b60015b6115bb57604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610789565b5f80516020611e4483398151915281146115eb57604051632a87526960e21b815260048101829052602401610789565b6113f883836117ab565b306001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b5c5760405163703e46dd60e11b815260040160405180910390fd5b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b6116b6611800565b610c6481611849565b610b5c611800565b6040516001600160a01b0384811660248301528381166044830152606482018390526117009186918216906323b872dd906084016113c6565b50505050565b5f81815b8451811015611740576117368286838151811061172957611729611df5565b6020026020010151611851565b915060010161170a565b5090505b92915050565b5f61175e6001600160a01b0384168361187a565b905080515f141580156117825750808060200190518101906117809190611e09565b155b156113f857604051635274afe760e01b81526001600160a01b0384166004820152602401610789565b6117b482611887565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156117f8576113f882826118ea565b610c8261195c565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610b5c57604051631afcd79f60e31b815260040160405180910390fd5b611319611800565b5f81831061186b575f828152602084905260409020611392565b505f9182526020526040902090565b606061139283835f61197b565b806001600160a01b03163b5f036118bc57604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610789565b5f80516020611e4483398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f80846001600160a01b0316846040516119069190611e28565b5f60405180830381855af49150503d805f811461193e576040519150601f19603f3d011682016040523d82523d5f602084013e611943565b606091505b5091509150611953858383611a14565b95945050505050565b3415610b5c5760405163b398979f60e01b815260040160405180910390fd5b6060814710156119a05760405163cd78605960e01b8152306004820152602401610789565b5f80856001600160a01b031684866040516119bb9190611e28565b5f6040518083038185875af1925050503d805f81146119f5576040519150601f19603f3d011682016040523d82523d5f602084013e6119fa565b606091505b5091509150611a0a868383611a14565b9695505050505050565b606082611a2957611a2482611a70565b611392565b8151158015611a4057506001600160a01b0384163b155b15611a6957604051639996b31560e01b81526001600160a01b0385166004820152602401610789565b5080611392565b805115611a805780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b80356001600160a01b0381168114611aaf575f80fd5b919050565b5f805f805f60808688031215611ac8575f80fd5b8535945060208601359350604086013567ffffffffffffffff80821115611aed575f80fd5b818801915088601f830112611b00575f80fd5b813581811115611b0e575f80fd5b8960208260051b8501011115611b22575f80fd5b602083019550809450505050611b3a60608701611a99565b90509295509295909350565b5f60208284031215611b56575f80fd5b61139282611a99565b5f60208284031215611b6f575f80fd5b5035919050565b634e487b7160e01b5f52604160045260245ffd5b5f8060408385031215611b9b575f80fd5b611ba483611a99565b9150602083013567ffffffffffffffff80821115611bc0575f80fd5b818501915085601f830112611bd3575f80fd5b813581811115611be557611be5611b76565b604051601f8201601f19908116603f01168101908382118183101715611c0d57611c0d611b76565b81604052828152886020848701011115611c25575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f6101c082019050825182526020830151602083015260408301516040830152606083015160608301526080830151608083015260a083015160a083015260c083015160c083015260e0830151611ca160e084018215159052565b5061010083810151151590830152610120808401511515908301526101408084015190830152610160808401519083015261018080840151908301526101a092830151929091019190915290565b5f5b83811015611d09578181015183820152602001611cf1565b50505f910152565b602081525f8251806020840152611d2f816040850160208701611cef565b601f01601f19169190910160400192915050565b5f8060408385031215611d54575f80fd5b50508035926020909101359150565b604080825281018390525f6001600160fb1b03841115611d81575f80fd5b8360051b808660608501376020830193909352500160600192915050565b634e487b7160e01b5f52601160045260245ffd5b8082018082111561174457611744611d9f565b5f60208284031215611dd6575f80fd5b5051919050565b5f60018201611dee57611dee611d9f565b5060010190565b634e487b7160e01b5f52603260045260245ffd5b5f60208284031215611e19575f80fd5b81518015158114611392575f80fd5b5f8251611e39818460208701611cef565b919091019291505056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00a26469706673582212208dfdba2e9477dfb8915251f3d74ef98c8ef7ce5548326403dda523156367f64764736f6c63430008180033000000000000000000000000010700808d59d2bb92257fcafacfe8e5bff7ab87000000000000000000000000793500709506652fcc61f0d2d0fda605638d429300000000000000000000000005dc0010c9902ecf6cbc921c6a4bd971c69e5a2e0000000000000000000000000000000000000000000000000000000067ab90200000000000000000000000000000000000000000000000000000000067b4caa00000000000000000000000000000000000000000000000000000003a35294400", + "nonce": "0x4bc8d", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xd5ea37bc7d2fa77fa1857835cbb8a8546793308c384103ea10800be1accd454c", + "transactionType": "CREATE2", + "contractName": "UUPSProxy", + "contractAddress": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "function": null, + "arguments": [ + "0xe9aB275D7E9859bbeF11A79b7c1854a030D0c171", + "0x" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "value": "0x0", + "input": "0x6b5d0bf482f03bfc69060956ae59c41746097a4c9eeee42aef2212b60bf28a9c608060405234801561000f575f80fd5b506040516104d43803806104d483398101604081905261002e916102e2565b818161003b82825f610044565b505050506103f7565b61004d8361006f565b5f825111806100595750805b1561006a5761006883836100ae565b505b505050565b610078816100da565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a250565b60606100d383836040518060600160405280602781526020016104ad6027913961018d565b9392505050565b6001600160a01b0381163b61014c5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b60648201526084015b60405180910390fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b0319166001600160a01b0392909216919091179055565b60605f80856001600160a01b0316856040516101a991906103aa565b5f60405180830381855af49150503d805f81146101e1576040519150601f19603f3d011682016040523d82523d5f602084013e6101e6565b606091505b5090925090506101f886838387610202565b9695505050505050565b606083156102705782515f03610269576001600160a01b0385163b6102695760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610143565b508161027a565b61027a8383610282565b949350505050565b8151156102925781518083602001fd5b8060405162461bcd60e51b815260040161014391906103c5565b634e487b7160e01b5f52604160045260245ffd5b5f5b838110156102da5781810151838201526020016102c2565b50505f910152565b5f80604083850312156102f3575f80fd5b82516001600160a01b0381168114610309575f80fd5b60208401519092506001600160401b0380821115610325575f80fd5b818501915085601f830112610338575f80fd5b81518181111561034a5761034a6102ac565b604051601f8201601f19908116603f01168101908382118183101715610372576103726102ac565b8160405282815288602084870101111561038a575f80fd5b61039b8360208301602088016102c0565b80955050505050509250929050565b5f82516103bb8184602087016102c0565b9190910192915050565b602081525f82518060208401526103e38160408501602087016102c0565b601f01601f19169190910160400192915050565b60aa806104035f395ff3fe608060405236601057600e6013565b005b600e5b601f601b6021565b6057565b565b5f60527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b365f80375f80365f845af43d5f803e8080156070573d5ff35b3d5ffdfea264697066735822122039fd63d084b1b3efb382c0e80fee4b3d1b88fe0f3a68a497d4090acbcdbdc35864736f6c63430008180033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564000000000000000000000000e9ab275d7e9859bbef11a79b7c1854a030d0c17100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x4bc8e", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "function": "handleOps((address,uint256,bytes,bytes,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],address)", + "arguments": [ + "[(0x2e2B1c42E38f5af81771e65D87729E57ABD1337a, 4433, 0x, 0xb61d27f60000000000000000000000005a2b641b84b0230c8e75f55d5afd27f4dbd59d5b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000084db9e99080000000000000000000000003e9727470c66b1e77034590926cde0242b5a3dcc000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f00000000000000000000000000000000000000000000000000000000, 4000000, 210000, 21000, 1, 1000000000, 0x0000000000000000000000000000000000000000, 0x390d43b95a1e78bb154cee777f78cdcd4236c4b3b07d6ce13f45037b819f5f8e4ef23a9ac2e4b0d727345561a48d294c61ea1a055437df47d4e14c80a3f108751b4dafe04e525669c20f12056b9e11a36666de86b464c4a95722ae6b09b459caaf2db9363477c007fbf6bef1106b2f74871b3150d7bc8405f84f91050335eb230d1b)]", + "0x660ad4B5A74130a4796B4d54BC6750Ae93C86e6c" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "value": "0x0", + "input": "0x1fad948c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000660ad4b5a74130a4796b4d54bc6750ae93c86e6c000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a00000000000000000000000000000000000000000000000000000000000011510000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000003d0900000000000000000000000000000000000000000000000000000000000003345000000000000000000000000000000000000000000000000000000000000052080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000124b61d27f60000000000000000000000005a2b641b84b0230c8e75f55d5afd27f4dbd59d5b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000084db9e99080000000000000000000000003e9727470c66b1e77034590926cde0242b5a3dcc000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000082390d43b95a1e78bb154cee777f78cdcd4236c4b3b07d6ce13f45037b819f5f8e4ef23a9ac2e4b0d727345561a48d294c61ea1a055437df47d4e14c80a3f108751b4dafe04e525669c20f12056b9e11a36666de86b464c4a95722ae6b09b459caaf2db9363477c007fbf6bef1106b2f74871b3150d7bc8405f84f91050335eb230d1b000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x4bc8f", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "function": "handleOps((address,uint256,bytes,bytes,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],address)", + "arguments": [ + "[(0x2e2B1c42E38f5af81771e65D87729E57ABD1337a, 4434, 0x, 0xb61d27f60000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000048129fc1c00000000000000000000000000000000000000000000000000000000, 4000000, 210000, 21000, 1, 1000000000, 0x0000000000000000000000000000000000000000, 0xe22c27fb22621ad86cbdc98b07b2977fd03de9855c2051ffbca24a9771efd13342bdb6864c9c47bb5a478875ce28793a5cc7401ce27c6272e2f68e19a2ba6eb21c)]", + "0x660ad4B5A74130a4796B4d54BC6750Ae93C86e6c" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "value": "0x0", + "input": "0x1fad948c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000660ad4b5a74130a4796b4d54bc6750ae93c86e6c000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a00000000000000000000000000000000000000000000000000000000000011520000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000003d0900000000000000000000000000000000000000000000000000000000000003345000000000000000000000000000000000000000000000000000000000000052080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4b61d27f60000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000048129fc1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041e22c27fb22621ad86cbdc98b07b2977fd03de9855c2051ffbca24a9771efd13342bdb6864c9c47bb5a478875ce28793a5cc7401ce27c6272e2f68e19a2ba6eb21c00000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x4bc90", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x2155b7", + "logs": [ + { + "address": "0xe9ab275d7e9859bbef11a79b7c1854a030d0c171", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", + "blockHash": "0xba15178f1757f24c0360486a2b373b2240bfb3ccb12704a5420d8b124c027f5b", + "blockNumber": "0xb1e66", + "transactionHash": "0x55f9c9c447311fbe329d2f01414445cdfff118ffe049dd1127a20dedbfaa1f3e", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000100000000000001000000000000000000000000100000000000000000000000000000", + "type": "0x2", + "transactionHash": "0x55f9c9c447311fbe329d2f01414445cdfff118ffe049dd1127a20dedbfaa1f3e", + "transactionIndex": "0x1", + "blockHash": "0xba15178f1757f24c0360486a2b373b2240bfb3ccb12704a5420d8b124c027f5b", + "blockNumber": "0xb1e66", + "gasUsed": "0x2155b7", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": null, + "contractAddress": "0xe9ab275d7e9859bbef11a79b7c1854a030d0c171", + "gasUsedForL1": "0x6243d", + "l1BlockNumber": "0x14caf89" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x39ca8", + "logs": [ + { + "address": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x000000000000000000000000e9ab275d7e9859bbef11a79b7c1854a030d0c171" + ], + "data": "0x", + "blockHash": "0x538925cecf0a66eeb5b1dad36a3d52c973ff8d288c140c98d19e382b27694674", + "blockNumber": "0xb1e67", + "transactionHash": "0xd5ea37bc7d2fa77fa1857835cbb8a8546793308c384103ea10800be1accd454c", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000400000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000020000000000000000000000010000000000000000000000000000000000000000000000020000000000000000000200000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xd5ea37bc7d2fa77fa1857835cbb8a8546793308c384103ea10800be1accd454c", + "transactionIndex": "0x1", + "blockHash": "0x538925cecf0a66eeb5b1dad36a3d52c973ff8d288c140c98d19e382b27694674", + "blockNumber": "0xb1e67", + "gasUsed": "0x39ca8", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": null, + "gasUsedForL1": "0x18cac", + "l1BlockNumber": "0x14caf89" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x42501", + "logs": [ + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972" + ], + "data": "0x", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0x5a2b641b84b0230c8e75f55d5afd27f4dbd59d5b", + "topics": [ + "0x5ed922fe11a834df4ad0556e8f265179bbe61f057f4808eab4c3b6542b621260", + "0x0000000000000000000000003e9727470c66b1e77034590926cde0242b5a3dcc" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "logIndex": "0x1", + "removed": false + }, + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f", + "0x04aeca23631dcf3b671dba54684da1d455df7229aa9a8f4d99598564da030af8", + "0x0000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000011510000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000008f82c000000000000000000000000000000000000000000000000000000000008f82c", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "logIndex": "0x2", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000008000000000000000000010000000000000002000000000000020000000000000000000000000000000000000000000000000000000000000000000008000800000000020000000000000000000800002000000000000000000000000000000000000000000000000002000000800000000000000000800000080000000000000400000000000000400000000000000000000000000000000002000000000000000000000000100001000000000000000000010000000800000000000020000000000000010000000200000000000000000000000000001000000084000000", + "type": "0x2", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "gasUsed": "0x42501", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "contractAddress": null, + "gasUsedForL1": "0xbc38", + "l1BlockNumber": "0x14caf89" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x391f6", + "logs": [ + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972" + ], + "data": "0x", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a" + ], + "data": "0x", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x1", + "removed": false + }, + { + "address": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x2", + "removed": false + }, + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f", + "0xc50415f27c552212c0a8a073aad2a632c28483f2a43c9a6345f581aeb70fbf43", + "0x0000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000001152000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000898ad00000000000000000000000000000000000000000000000000000000000898ad", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x3", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000408000000000000000000010000000000000000000000000000020000000000000000000000000000000000000000000001000000000000000000000000000800000000020000000000000000000800002000000000000000000000000000400000000000000000000800000000800000000000000080000000000000000020000400000000000010400010000000000000000000000000000002000000000000000000000000100001000000000000000000000004010000000000000020000000000000010000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "gasUsed": "0x391f6", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "contractAddress": null, + "gasUsedForL1": "0x9508", + "l1BlockNumber": "0x14caf8a" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1739030918, + "chain": 7887, + "commit": "d16c447" +} \ No newline at end of file diff --git a/broadcast/155-deploy-sale.s.sol/7887/run-latest.json b/broadcast/155-deploy-sale.s.sol/7887/run-latest.json new file mode 100644 index 00000000..1f85eff9 --- /dev/null +++ b/broadcast/155-deploy-sale.s.sol/7887/run-latest.json @@ -0,0 +1,302 @@ +{ + "transactions": [ + { + "hash": "0x55f9c9c447311fbe329d2f01414445cdfff118ffe049dd1127a20dedbfaa1f3e", + "transactionType": "CREATE", + "contractName": "SealedBidTokenSale", + "contractAddress": "0xe9ab275d7e9859bbef11a79b7c1854a030d0c171", + "function": null, + "arguments": [ + "0x010700808D59d2bb92257fCafACfe8e5bFF7aB87", + "0x793500709506652Fcc61F0d2D0fDa605638D4293", + "0x05DC0010C9902EcF6CBc921c6A4bd971c69E5A2E", + "1739296800", + "1739901600", + "250000000000" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "value": "0x0", + "input": "0x6101606040523060805234801562000015575f80fd5b50604051620021bd380380620021bd8339810160408190526200003891620001d2565b6200004262000102565b6001600160a01b0386166200007a576040516307094c3360e11b81526001600160a01b03871660048201526024015b60405180910390fd5b6001600160a01b038516620000ae576040516334d5d27d60e21b81526001600160a01b038616600482015260240162000071565b818310620000cf57604051631800812760e11b815260040160405180910390fd5b6001600160a01b0395861660a05293851660e0529190931660c05261010092909252610120919091526101405262000234565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff1615620001535760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b0390811614620001b35780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b80516001600160a01b0381168114620001cd575f80fd5b919050565b5f805f805f8060c08789031215620001e8575f80fd5b620001f387620001b6565b95506200020360208801620001b6565b94506200021360408801620001b6565b9350606087015192506080870151915060a087015190509295509295509295565b60805160a05160c05160e051610100516101205161014051611eb9620003045f395f8181610689015281816109a90152610d6601525f818161048401528181610d3c01526111a301525f81816102ec01528181610b6801528181610ba301528181610d1901528181611109015261114401525f8181610388015261101601525f81816104ff015281816108d501528181610ae901528181611040015281816110b3015261129601525f8181610622015261088401525f8181611498015281816114c101526116000152611eb95ff3fe608060405260043610610207575f3560e01c806378e9792511610113578063ad3cb1cc1161009d578063ecfd89281161006d578063ecfd892814610644578063f2fde38b14610659578063f381f2a514610678578063fc7e286d146106ab578063ff50abdc146106d6575f80fd5b8063ad3cb1cc1461059e578063e1e158a5146105db578063e2bbb158146105f2578063e985e36714610611575f80fd5b806389a30271116100e357806389a30271146104ee5780638bd29968146105215780638da5cb5b146105365780639038e693146105725780639b8906ae14610586575f80fd5b806378e97925146104735780637cb64759146104a65780638129fc1c146104c557806388de2330146104d9575f80fd5b80634f93594511610194578063665828021161016457806366582802146103c25780636bb8a6e0146103ee578063715018a61461041c578063734d6db31461043057806373b2e80e14610445575f80fd5b80634f9359451461032157806352d1902d1461034e57806358334e291461036257806361d027b314610377575f80fd5b80634602d509116101da5780634602d5091461027c578063469619f2146102a75780634b319713146102c65780634dc41210146102db5780634f1ef2861461030e575f80fd5b80632496903c1461020b5780632eb4a7ab1461022c578063380d831b146102545780633ccfd60b14610268575b5f80fd5b348015610216575f80fd5b5061022a610225366004611ab4565b6106eb565b005b348015610237575f80fd5b5061024160055481565b6040519081526020015b60405180910390f35b34801561025f575f80fd5b5061022a610974565b348015610273575f80fd5b5061022a610a2d565b348015610287575f80fd5b50610241610296366004611b46565b60086020525f908152604090205481565b3480156102b2575f80fd5b5061022a6102c1366004611b5f565b610b5e565b3480156102d1575f80fd5b5061024160015481565b3480156102e6575f80fd5b506102417f000000000000000000000000000000000000000000000000000000000000000081565b61022a61031c366004611b8a565b610c67565b34801561032c575f80fd5b505f5461033e90610100900460ff1681565b604051901515815260200161024b565b348015610359575f80fd5b50610241610c86565b34801561036d575f80fd5b5061024160035481565b348015610382575f80fd5b506103aa7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b03909116815260200161024b565b3480156103cd575f80fd5b506103e16103dc366004611b46565b610ca1565b60405161024b9190611c46565b3480156103f9575f80fd5b5061033e610408366004611b46565b600b6020525f908152604090205460ff1681565b348015610427575f80fd5b5061022a610e21565b34801561043b575f80fd5b5061024160045481565b348015610450575f80fd5b5061033e61045f366004611b46565b60076020525f908152604090205460ff1681565b34801561047e575f80fd5b506102417f000000000000000000000000000000000000000000000000000000000000000081565b3480156104b1575f80fd5b5061022a6104c0366004611b5f565b610e32565b3480156104d0575f80fd5b5061022a610eab565b3480156104e4575f80fd5b50610241600a5481565b3480156104f9575f80fd5b506103aa7f000000000000000000000000000000000000000000000000000000000000000081565b34801561052c575f80fd5b506102416102bc81565b348015610541575f80fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b03166103aa565b34801561057d575f80fd5b5061022a610fbf565b348015610591575f80fd5b505f5461033e9060ff1681565b3480156105a9575f80fd5b506105ce604051806040016040528060058152602001640352e302e360dc1b81525081565b60405161024b9190611d11565b3480156105e6575f80fd5b50610241630ee6b28081565b3480156105fd575f80fd5b5061022a61060c366004611d43565b6110da565b34801561061c575f80fd5b506103aa7f000000000000000000000000000000000000000000000000000000000000000081565b34801561064f575f80fd5b5061024160095481565b348015610664575f80fd5b5061022a610673366004611b46565b611311565b348015610683575f80fd5b506102417f000000000000000000000000000000000000000000000000000000000000000081565b3480156106b6575f80fd5b506102416106c5366004611b46565b60066020525f908152604090205481565b3480156106e1575f80fd5b5061024160025481565b6106f361134b565b5f5460ff16158061070b57505f54610100900460ff16155b156107295760405163b4202dd560e01b815260040160405180910390fd5b60055461074957604051634fc5147960e11b815260040160405180910390fd5b6001600160a01b0381165f9081526007602052604090205460ff161561079257604051632058b6db60e01b81526001600160a01b03821660048201526024015b60405180910390fd5b604080516001600160a01b0383166020820152908101869052606081018590525f9060800160408051601f198184030181528282528051602091820120908301520160405160208183030381529060405280519060200120905061082c8484808060200260200160405190810160405280939291908181526020018383602002808284375f92019190915250506005549150849050611382565b61084f5783838260405163571e214960e11b815260040161078993929190611d63565b6001600160a01b0382165f908152600760205260409020805460ff1916600117905585156108c2576108ab6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168388611399565b8560045f8282546108bc9190611db3565b90915550505b8415610913576108fc6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000168387611399565b8460035f82825461090d9190611db3565b90915550505b816001600160a01b03167fd8138f8a3f377c5259ca548e70e4c2de94f129f5a11036a15b69513cba2b426a8760405161094e91815260200190565b60405180910390a25061096d60015f80516020611e6483398151915255565b5050505050565b61097c6113fd565b5f5460ff16156109a157604051632252c26960e21b8152426004820152602401610789565b5f80546002547f000000000000000000000000000000000000000000000000000000000000000081101561010090810261ffff1990931692909217600117928390556040517f8233a98787b42c8a85877e43d0969d9758e213000c07b8e1db21707dd06d05d193610a2393900460ff1691909115158252602082015260400190565b60405180910390a1565b610a3561134b565b5f5460ff16610a59576040516336f9bbd760e11b8152426004820152602401610789565b5f54610100900460ff1615610a8157604051636bf4c8e960e11b815260040160405180910390fd5b335f9081526006602052604081205490819003610ab357604051636e34ee0b60e11b8152336004820152602401610789565b335f90815260066020526040812081905560018054839290610ad6908490611db3565b90915550610b1090506001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163383611399565b60405181815233907f7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d59060200160405180910390a250610b5c60015f80516020611e6483398151915255565b565b610b6661134b565b7f0000000000000000000000000000000000000000000000000000000000000000421015610bcf5760405163457f873160e01b81524260048201527f00000000000000000000000000000000000000000000000000000000000000006024820152604401610789565b5f5460ff1615610bf457604051632252c26960e21b8152426004820152602401610789565b610bfd81611458565b335f81815260086020908152604091829020805490859055825181815291820185905292917f18332e48c9676c7696287f3a4e7bcd1cb6cad945dd233d5711ab89cf106b673c910160405180910390a250610c6460015f80516020611e6483398151915255565b50565b610c6f61148d565b610c7882611531565b610c828282611539565b5050565b5f610c8f6115f5565b505f80516020611e4483398151915290565b610d0b604051806101c001604052805f81526020015f81526020015f81526020015f81526020015f81526020015f81526020015f81526020015f151581526020015f151581526020015f151581526020015f81526020015f81526020015f81526020015f81525090565b50604080516101c0810182527f000000000000000000000000000000000000000000000000000000000000000081527f00000000000000000000000000000000000000000000000000000000000000006020808301919091527f0000000000000000000000000000000000000000000000000000000000000000828401526002546060830152600154608083015260035460a083015260045460c08301525f805460ff808216151560e08601526101009182900481161515918501919091526001600160a01b0390951680825260078352848220549095161515610120840152600954610140840152600a546101608401528481526006825283812054610180840152938452600890529120546101a082015290565b610e296113fd565b610b5c5f61163e565b610e3a6113fd565b5f5460ff161580610e5257505f54610100900460ff16155b15610e705760405163b4202dd560e01b815260040160405180910390fd5b60058190556040518181527f42cbc405e4dbf1b691e85b9a34b08ecfcf7a9ad9078bf4d645ccfa1fac11c10b9060200160405180910390a150565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f81158015610ef05750825b90505f8267ffffffffffffffff166001148015610f0c5750303b155b905081158015610f1a575080155b15610f385760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff191660011785558315610f6257845460ff60401b1916600160401b1785555b610f6b336116ae565b610f736116bf565b831561096d57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15050505050565b610fc76113fd565b5f5460ff161580610fdf57505f54610100900460ff16155b15610ffd5760405163b4202dd560e01b815260040160405180910390fd5b6040516370a0823160e01b8152306004820152610b5c907f0000000000000000000000000000000000000000000000000000000000000000906001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906370a0823190602401602060405180830381865afa158015611085573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906110a99190611dc6565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169190611399565b6110e261134b565b5f5460ff161561110757604051632252c26960e21b8152426004820152602401610789565b7f00000000000000000000000000000000000000000000000000000000000000004210156111705760405163457f873160e01b81524260048201527f00000000000000000000000000000000000000000000000000000000000000006024820152604401610789565b630ee6b28082101561119857604051630266752760e51b815260048101839052602401610789565b6111a181611458565b7f0000000000000000000000000000000000000000000000000000000000000000421015611230576102bc600a54106111ed5760405163b64bf0e160e01b815260040160405180910390fd5b335f908152600b602052604090205460ff1661123057335f908152600b60205260408120805460ff19166001179055600a80549161122a83611ddd565b91905055505b335f908152600660205260408120805484929061124e908490611db3565b925050819055508160025f8282546112669190611db3565b909155505060098054905f61127a83611ddd565b9091555050335f8181526008602052604090208290556112c6907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169030856116c7565b60405182815233907f2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c49060200160405180910390a2610c8260015f80516020611e6483398151915255565b6113196113fd565b6001600160a01b03811661134257604051631e4fbdf760e01b81525f6004820152602401610789565b610c648161163e565b5f80516020611e6483398151915280546001190161137c57604051633ee5aeb560e01b815260040160405180910390fd5b60029055565b5f8261138e8584611706565b1490505b9392505050565b6040516001600160a01b038381166024830152604482018390526113f891859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b03838183161783525050505061174a565b505050565b3361142f7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b031614610b5c5760405163118cdaa760e01b8152336004820152602401610789565b6298968081108061146c57506301c9c38081115b15610c64576040516319bfbdbd60e11b815260048101829052602401610789565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061151357507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166115075f80516020611e44833981519152546001600160a01b031690565b6001600160a01b031614155b15610b5c5760405163703e46dd60e11b815260040160405180910390fd5b610c646113fd565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015611593575060408051601f3d908101601f1916820190925261159091810190611dc6565b60015b6115bb57604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610789565b5f80516020611e4483398151915281146115eb57604051632a87526960e21b815260048101829052602401610789565b6113f883836117ab565b306001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b5c5760405163703e46dd60e11b815260040160405180910390fd5b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b6116b6611800565b610c6481611849565b610b5c611800565b6040516001600160a01b0384811660248301528381166044830152606482018390526117009186918216906323b872dd906084016113c6565b50505050565b5f81815b8451811015611740576117368286838151811061172957611729611df5565b6020026020010151611851565b915060010161170a565b5090505b92915050565b5f61175e6001600160a01b0384168361187a565b905080515f141580156117825750808060200190518101906117809190611e09565b155b156113f857604051635274afe760e01b81526001600160a01b0384166004820152602401610789565b6117b482611887565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156117f8576113f882826118ea565b610c8261195c565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610b5c57604051631afcd79f60e31b815260040160405180910390fd5b611319611800565b5f81831061186b575f828152602084905260409020611392565b505f9182526020526040902090565b606061139283835f61197b565b806001600160a01b03163b5f036118bc57604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610789565b5f80516020611e4483398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f80846001600160a01b0316846040516119069190611e28565b5f60405180830381855af49150503d805f811461193e576040519150601f19603f3d011682016040523d82523d5f602084013e611943565b606091505b5091509150611953858383611a14565b95945050505050565b3415610b5c5760405163b398979f60e01b815260040160405180910390fd5b6060814710156119a05760405163cd78605960e01b8152306004820152602401610789565b5f80856001600160a01b031684866040516119bb9190611e28565b5f6040518083038185875af1925050503d805f81146119f5576040519150601f19603f3d011682016040523d82523d5f602084013e6119fa565b606091505b5091509150611a0a868383611a14565b9695505050505050565b606082611a2957611a2482611a70565b611392565b8151158015611a4057506001600160a01b0384163b155b15611a6957604051639996b31560e01b81526001600160a01b0385166004820152602401610789565b5080611392565b805115611a805780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b80356001600160a01b0381168114611aaf575f80fd5b919050565b5f805f805f60808688031215611ac8575f80fd5b8535945060208601359350604086013567ffffffffffffffff80821115611aed575f80fd5b818801915088601f830112611b00575f80fd5b813581811115611b0e575f80fd5b8960208260051b8501011115611b22575f80fd5b602083019550809450505050611b3a60608701611a99565b90509295509295909350565b5f60208284031215611b56575f80fd5b61139282611a99565b5f60208284031215611b6f575f80fd5b5035919050565b634e487b7160e01b5f52604160045260245ffd5b5f8060408385031215611b9b575f80fd5b611ba483611a99565b9150602083013567ffffffffffffffff80821115611bc0575f80fd5b818501915085601f830112611bd3575f80fd5b813581811115611be557611be5611b76565b604051601f8201601f19908116603f01168101908382118183101715611c0d57611c0d611b76565b81604052828152886020848701011115611c25575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f6101c082019050825182526020830151602083015260408301516040830152606083015160608301526080830151608083015260a083015160a083015260c083015160c083015260e0830151611ca160e084018215159052565b5061010083810151151590830152610120808401511515908301526101408084015190830152610160808401519083015261018080840151908301526101a092830151929091019190915290565b5f5b83811015611d09578181015183820152602001611cf1565b50505f910152565b602081525f8251806020840152611d2f816040850160208701611cef565b601f01601f19169190910160400192915050565b5f8060408385031215611d54575f80fd5b50508035926020909101359150565b604080825281018390525f6001600160fb1b03841115611d81575f80fd5b8360051b808660608501376020830193909352500160600192915050565b634e487b7160e01b5f52601160045260245ffd5b8082018082111561174457611744611d9f565b5f60208284031215611dd6575f80fd5b5051919050565b5f60018201611dee57611dee611d9f565b5060010190565b634e487b7160e01b5f52603260045260245ffd5b5f60208284031215611e19575f80fd5b81518015158114611392575f80fd5b5f8251611e39818460208701611cef565b919091019291505056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00a26469706673582212208dfdba2e9477dfb8915251f3d74ef98c8ef7ce5548326403dda523156367f64764736f6c63430008180033000000000000000000000000010700808d59d2bb92257fcafacfe8e5bff7ab87000000000000000000000000793500709506652fcc61f0d2d0fda605638d429300000000000000000000000005dc0010c9902ecf6cbc921c6a4bd971c69e5a2e0000000000000000000000000000000000000000000000000000000067ab90200000000000000000000000000000000000000000000000000000000067b4caa00000000000000000000000000000000000000000000000000000003a35294400", + "nonce": "0x4bc8d", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xd5ea37bc7d2fa77fa1857835cbb8a8546793308c384103ea10800be1accd454c", + "transactionType": "CREATE2", + "contractName": "UUPSProxy", + "contractAddress": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "function": null, + "arguments": [ + "0xe9aB275D7E9859bbeF11A79b7c1854a030D0c171", + "0x" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "value": "0x0", + "input": "0x6b5d0bf482f03bfc69060956ae59c41746097a4c9eeee42aef2212b60bf28a9c608060405234801561000f575f80fd5b506040516104d43803806104d483398101604081905261002e916102e2565b818161003b82825f610044565b505050506103f7565b61004d8361006f565b5f825111806100595750805b1561006a5761006883836100ae565b505b505050565b610078816100da565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a250565b60606100d383836040518060600160405280602781526020016104ad6027913961018d565b9392505050565b6001600160a01b0381163b61014c5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b60648201526084015b60405180910390fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b0319166001600160a01b0392909216919091179055565b60605f80856001600160a01b0316856040516101a991906103aa565b5f60405180830381855af49150503d805f81146101e1576040519150601f19603f3d011682016040523d82523d5f602084013e6101e6565b606091505b5090925090506101f886838387610202565b9695505050505050565b606083156102705782515f03610269576001600160a01b0385163b6102695760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610143565b508161027a565b61027a8383610282565b949350505050565b8151156102925781518083602001fd5b8060405162461bcd60e51b815260040161014391906103c5565b634e487b7160e01b5f52604160045260245ffd5b5f5b838110156102da5781810151838201526020016102c2565b50505f910152565b5f80604083850312156102f3575f80fd5b82516001600160a01b0381168114610309575f80fd5b60208401519092506001600160401b0380821115610325575f80fd5b818501915085601f830112610338575f80fd5b81518181111561034a5761034a6102ac565b604051601f8201601f19908116603f01168101908382118183101715610372576103726102ac565b8160405282815288602084870101111561038a575f80fd5b61039b8360208301602088016102c0565b80955050505050509250929050565b5f82516103bb8184602087016102c0565b9190910192915050565b602081525f82518060208401526103e38160408501602087016102c0565b601f01601f19169190910160400192915050565b60aa806104035f395ff3fe608060405236601057600e6013565b005b600e5b601f601b6021565b6057565b565b5f60527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b365f80375f80365f845af43d5f803e8080156070573d5ff35b3d5ffdfea264697066735822122039fd63d084b1b3efb382c0e80fee4b3d1b88fe0f3a68a497d4090acbcdbdc35864736f6c63430008180033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564000000000000000000000000e9ab275d7e9859bbef11a79b7c1854a030d0c17100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x4bc8e", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "function": "handleOps((address,uint256,bytes,bytes,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],address)", + "arguments": [ + "[(0x2e2B1c42E38f5af81771e65D87729E57ABD1337a, 4433, 0x, 0xb61d27f60000000000000000000000005a2b641b84b0230c8e75f55d5afd27f4dbd59d5b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000084db9e99080000000000000000000000003e9727470c66b1e77034590926cde0242b5a3dcc000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f00000000000000000000000000000000000000000000000000000000, 4000000, 210000, 21000, 1, 1000000000, 0x0000000000000000000000000000000000000000, 0x390d43b95a1e78bb154cee777f78cdcd4236c4b3b07d6ce13f45037b819f5f8e4ef23a9ac2e4b0d727345561a48d294c61ea1a055437df47d4e14c80a3f108751b4dafe04e525669c20f12056b9e11a36666de86b464c4a95722ae6b09b459caaf2db9363477c007fbf6bef1106b2f74871b3150d7bc8405f84f91050335eb230d1b)]", + "0x660ad4B5A74130a4796B4d54BC6750Ae93C86e6c" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "value": "0x0", + "input": "0x1fad948c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000660ad4b5a74130a4796b4d54bc6750ae93c86e6c000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a00000000000000000000000000000000000000000000000000000000000011510000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000003d0900000000000000000000000000000000000000000000000000000000000003345000000000000000000000000000000000000000000000000000000000000052080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000124b61d27f60000000000000000000000005a2b641b84b0230c8e75f55d5afd27f4dbd59d5b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000084db9e99080000000000000000000000003e9727470c66b1e77034590926cde0242b5a3dcc000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000082390d43b95a1e78bb154cee777f78cdcd4236c4b3b07d6ce13f45037b819f5f8e4ef23a9ac2e4b0d727345561a48d294c61ea1a055437df47d4e14c80a3f108751b4dafe04e525669c20f12056b9e11a36666de86b464c4a95722ae6b09b459caaf2db9363477c007fbf6bef1106b2f74871b3150d7bc8405f84f91050335eb230d1b000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x4bc8f", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + }, + { + "hash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionType": "CALL", + "contractName": null, + "contractAddress": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "function": "handleOps((address,uint256,bytes,bytes,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[],address)", + "arguments": [ + "[(0x2e2B1c42E38f5af81771e65D87729E57ABD1337a, 4434, 0x, 0xb61d27f60000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000048129fc1c00000000000000000000000000000000000000000000000000000000, 4000000, 210000, 21000, 1, 1000000000, 0x0000000000000000000000000000000000000000, 0xe22c27fb22621ad86cbdc98b07b2977fd03de9855c2051ffbca24a9771efd13342bdb6864c9c47bb5a478875ce28793a5cc7401ce27c6272e2f68e19a2ba6eb21c)]", + "0x660ad4B5A74130a4796B4d54BC6750Ae93C86e6c" + ], + "transaction": { + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "value": "0x0", + "input": "0x1fad948c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000660ad4b5a74130a4796b4d54bc6750ae93c86e6c000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a00000000000000000000000000000000000000000000000000000000000011520000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000003d0900000000000000000000000000000000000000000000000000000000000003345000000000000000000000000000000000000000000000000000000000000052080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4b61d27f60000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000048129fc1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041e22c27fb22621ad86cbdc98b07b2977fd03de9855c2051ffbca24a9771efd13342bdb6864c9c47bb5a478875ce28793a5cc7401ce27c6272e2f68e19a2ba6eb21c00000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x4bc90", + "chainId": "0x1ecf" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x2155b7", + "logs": [ + { + "address": "0xe9ab275d7e9859bbef11a79b7c1854a030d0c171", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", + "blockHash": "0xba15178f1757f24c0360486a2b373b2240bfb3ccb12704a5420d8b124c027f5b", + "blockNumber": "0xb1e66", + "transactionHash": "0x55f9c9c447311fbe329d2f01414445cdfff118ffe049dd1127a20dedbfaa1f3e", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000100000000000001000000000000000000000000100000000000000000000000000000", + "type": "0x2", + "transactionHash": "0x55f9c9c447311fbe329d2f01414445cdfff118ffe049dd1127a20dedbfaa1f3e", + "transactionIndex": "0x1", + "blockHash": "0xba15178f1757f24c0360486a2b373b2240bfb3ccb12704a5420d8b124c027f5b", + "blockNumber": "0xb1e66", + "gasUsed": "0x2155b7", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": null, + "contractAddress": "0xe9ab275d7e9859bbef11a79b7c1854a030d0c171", + "gasUsedForL1": "0x6243d", + "l1BlockNumber": "0x14caf89" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x39ca8", + "logs": [ + { + "address": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x000000000000000000000000e9ab275d7e9859bbef11a79b7c1854a030d0c171" + ], + "data": "0x", + "blockHash": "0x538925cecf0a66eeb5b1dad36a3d52c973ff8d288c140c98d19e382b27694674", + "blockNumber": "0xb1e67", + "transactionHash": "0xd5ea37bc7d2fa77fa1857835cbb8a8546793308c384103ea10800be1accd454c", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000400000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000020000000000000000000000010000000000000000000000000000000000000000000000020000000000000000000200000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xd5ea37bc7d2fa77fa1857835cbb8a8546793308c384103ea10800be1accd454c", + "transactionIndex": "0x1", + "blockHash": "0x538925cecf0a66eeb5b1dad36a3d52c973ff8d288c140c98d19e382b27694674", + "blockNumber": "0xb1e67", + "gasUsed": "0x39ca8", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c", + "contractAddress": null, + "gasUsedForL1": "0x18cac", + "l1BlockNumber": "0x14caf89" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x42501", + "logs": [ + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972" + ], + "data": "0x", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0x5a2b641b84b0230c8e75f55d5afd27f4dbd59d5b", + "topics": [ + "0x5ed922fe11a834df4ad0556e8f265179bbe61f057f4808eab4c3b6542b621260", + "0x0000000000000000000000003e9727470c66b1e77034590926cde0242b5a3dcc" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005a1e00884e35bf2dc39af51712d08bef24b1817f", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "logIndex": "0x1", + "removed": false + }, + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f", + "0x04aeca23631dcf3b671dba54684da1d455df7229aa9a8f4d99598564da030af8", + "0x0000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000011510000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000008f82c000000000000000000000000000000000000000000000000000000000008f82c", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "logIndex": "0x2", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000008000000000000000000010000000000000002000000000000020000000000000000000000000000000000000000000000000000000000000000000008000800000000020000000000000000000800002000000000000000000000000000000000000000000000000002000000800000000000000000800000080000000000000400000000000000400000000000000000000000000000000002000000000000000000000000100001000000000000000000010000000800000000000020000000000000010000000200000000000000000000000000001000000084000000", + "type": "0x2", + "transactionHash": "0xe6125be646cef90350f3c376b3aa689105017ec160c7b6ff5bbc1f41ad11ef54", + "transactionIndex": "0x1", + "blockHash": "0xd402b1e97a4a2193007a52324784a69ab63952a7b352fe0490e7339e8890192c", + "blockNumber": "0xb1e68", + "gasUsed": "0x42501", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "contractAddress": null, + "gasUsedForL1": "0xbc38", + "l1BlockNumber": "0x14caf89" + }, + { + "status": "0x1", + "cumulativeGasUsed": "0x391f6", + "logs": [ + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0xbb47ee3e183a558b1a2ff0874b079f3fc5478b7454eacf2bfc5af2ff5878f972" + ], + "data": "0x", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x0", + "removed": false + }, + { + "address": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a" + ], + "data": "0x", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x1", + "removed": false + }, + { + "address": "0x5a1e00884e35bf2dc39af51712d08bef24b1817f", + "topics": [ + "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x2", + "removed": false + }, + { + "address": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "topics": [ + "0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f", + "0xc50415f27c552212c0a8a073aad2a632c28483f2a43c9a6345f581aeb70fbf43", + "0x0000000000000000000000002e2b1c42e38f5af81771e65d87729e57abd1337a", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000001152000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000898ad00000000000000000000000000000000000000000000000000000000000898ad", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "logIndex": "0x3", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000408000000000000000000010000000000000000000000000000020000000000000000000000000000000000000000000001000000000000000000000000000800000000020000000000000000000800002000000000000000000000000000400000000000000000000800000000800000000000000080000000000000000020000400000000000010400010000000000000000000000000000002000000000000000000000000100001000000000000000000000004010000000000000020000000000000010000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xc9040ceaa02cb5c0cc139742d836d534d954628fe98eb9a930738c94d7646486", + "transactionIndex": "0x1", + "blockHash": "0x899762a7bdc297c47cbd27985b5b8eca4c6f34cb506e6bd4463b08832fdb54e2", + "blockNumber": "0xb1e69", + "gasUsed": "0x391f6", + "effectiveGasPrice": "0x5f5e100", + "from": "0x660ad4b5a74130a4796b4d54bc6750ae93c86e6c", + "to": "0x2843c269d2a64ecfa63548e8b3fc0fd23b7f70cb", + "contractAddress": null, + "gasUsedForL1": "0x9508", + "l1BlockNumber": "0x14caf8a" + } + ], + "libraries": [], + "pending": [], + "returns": {}, + "timestamp": 1739030918, + "chain": 7887, + "commit": "d16c447" +} \ No newline at end of file diff --git a/script/migrations/155-deploy-sale.s.sol b/script/migrations/155-deploy-sale.s.sol new file mode 100644 index 00000000..4c6108df --- /dev/null +++ b/script/migrations/155-deploy-sale.s.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import {SealedBidTokenSale} from "@kinto-core/apps/SealedBidTokenSale.sol"; + +import {SafeBeaconProxy} from "@kinto-core/proxy/SafeBeaconProxy.sol"; +import {KintoAppRegistry} from "@kinto-core/apps/KintoAppRegistry.sol"; + +import {UUPSProxy} from "@kinto-core-test/helpers/UUPSProxy.sol"; +import {MigrationHelper} from "@kinto-core-script/utils/MigrationHelper.sol"; + +import "@kinto-core-test/helpers/ArrayHelpers.sol"; +import {Script} from "forge-std/Script.sol"; +import {console2} from "forge-std/console2.sol"; + +contract DeployScript is Script, MigrationHelper { + using ArrayHelpers for *; + + address public constant SOCKET_APP = 0x3e9727470C66B1e77034590926CDe0242B5A3dCc; + address public constant KINTO = 0x010700808D59d2bb92257fCafACfe8e5bFF7aB87; + address public constant TREASURY = 0x793500709506652Fcc61F0d2D0fDa605638D4293; + address public constant USDC = 0x05DC0010C9902EcF6CBc921c6A4bd971c69E5A2E; + //Tuesday, Feb 11, 2025, 10:00:00 AM PT. + uint256 public constant PRE_START_TIME = 1739296800; + //Tuesday, Feb 18, 2025, 10:00:00 AM PT. + uint256 public constant START_TIME = 1739901600; + uint256 public constant MINIMUM_CAP = 250_000 * 1e6; + + function run() public override { + super.run(); + + if (_getChainDeployment("SealedBidTokenSale") != address(0)) { + console2.log("SealedBidTokenSale is deployed"); + return; + } + + vm.broadcast(deployerPrivateKey); + SealedBidTokenSale impl = new SealedBidTokenSale(KINTO, TREASURY, USDC, PRE_START_TIME, START_TIME, MINIMUM_CAP); + + (bytes32 salt, address expectedAddress) = + mineSalt(keccak256(abi.encodePacked(type(UUPSProxy).creationCode, abi.encode(address(impl), ""))), "5A1E00"); + + vm.broadcast(deployerPrivateKey); + UUPSProxy proxy = new UUPSProxy{salt: salt}(address(impl), ""); + SealedBidTokenSale sale = SealedBidTokenSale(address(proxy)); + + _handleOps( + abi.encodeWithSelector( + KintoAppRegistry.addAppContracts.selector, SOCKET_APP, [address(sale)].toMemoryArray() + ), + address(_getChainDeployment("KintoAppRegistry")) + ); + + uint256[] memory privateKeys = new uint256[](1); + privateKeys[0] = deployerPrivateKey; + _handleOps( + abi.encodeWithSelector(SealedBidTokenSale.initialize.selector), + payable(kintoAdminWallet), + address(proxy), + 0, + address(0), + privateKeys + ); + + assertEq(address(sale), address(expectedAddress)); + assertEq(address(sale.USDC()), USDC); + + saveContractAddress("SealedBidTokenSale", address(sale)); + saveContractAddress("SealedBidTokenSale-impl", address(impl)); + } +} diff --git a/test/artifacts/7887/addresses.json b/test/artifacts/7887/addresses.json index 0cbace32..a4b85722 100644 --- a/test/artifacts/7887/addresses.json +++ b/test/artifacts/7887/addresses.json @@ -264,5 +264,7 @@ "CRV": "0xC90000A619e56D12B9da6858509BA497B64e77eC", "BridgerL2V12-impl": "0xB0AC6E846079FA2A984298C056F304070EA24e31", "BridgerL2V13-impl": "0xfcdF95304e95aFb40d14300d39c258dB45194734", - "KintoIDV11-impl": "0x4aC06254558e144C41461a319822993900cE2eE4" + "KintoIDV11-impl": "0x4aC06254558e144C41461a319822993900cE2eE4", + "SealedBidTokenSale": "0x5a1E00884e35bF2dC39Af51712D08bEF24b1817f", + "SealedBidTokenSale-impl": "0xe9aB275D7E9859bbeF11A79b7c1854a030D0c171" } \ No newline at end of file