Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 60 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ The migration system consists of two main components:
- **Framework**: Foundry, with Hardhat for deployments
- **Proxy Pattern**: OpenZeppelin Transparent Upgradeable Proxy
- **Verification Mechanism**: Merkle Tree for secure, gas-efficient verification
- **Security**: OpenZeppelin contract libraries

## Contract Details

Expand All @@ -46,20 +45,45 @@ The MigrationLocker contract is responsible for allowing users to lock their PUS

**Key Features:**
- Token locking mechanism with unique identifier generation
- Safety toggles to prevent/allow locking
- Safety toggles to prevent/allow locking - Owner Controlled
- Proper access control with Ownable2Step pattern
- Token burning capability for migrated tokens
- Emergency fund recovery functionality

**Main Functions:**
- `lock(uint _amount, address _recipient)`: Allows users to lock tokens for migration
- `burn(uint _amount)`: Burns tokens that have been successfully migrated
- `setToggleLock()`: Toggles whether the contract is accepting new locks
- `pause()`: Pauses the contract to prevent new locks
- `unpause()`: Unpauses the contract to allow new locks
- `initiateNewEpoch()`: Starts a new epoch for organizing locks
- `recoverFunds(address _token, address _to, uint _amount)`: Emergency function to recover funds

**Events:**
- `Locked(address recipient, uint amount, uint indexed id)`: Emitted when tokens are locked
- `Locked(address caller, address recipient, uint amount, uint epoch)`: Emitted when tokens are locked

### Epoch System in MigrationLocker

The MigrationLocker contract uses an epoch-based system to organize token locks into time periods for efficient Merkle Tree generation.

It should be noted, however, that epochs are owner-controlled.

**How it Works:**
- Each epoch represents a specific time period for token locking
- The current epoch is recorded when users lock tokens via the `Locked` event
- Owners can start new epochs using `initiateNewEpoch()`
- Each epoch tracks its start block for off-chain event processing
- Epochs help organize locks into batches for systematic Merkle Tree generation and verification

This system ensures organized processing of token locks across different time periods.

**Expected Workflow**
1. Owner initiates LOCKING with `initiateNewEpoch()`, i.e., Epoch-1
2. Duration for how long EPOCH-1 will run is flexible and decided by owner, hence owner-controlled.
3. Users starts locking token in EPOCH-1. All such locks emit out `Locked(msg.sender, _recipient, _amount, 1)`. These event and params will be used for merkle proof creation.
4. After 30 days, for example, owner initiates pause() and triggers `initiateNewEpoch(). i.e., EPOCH-2.
5. Same locking cycle starts again, but now locks emits out `Locked(msg.sender, _recipient, _amount, 2)`.

---
### MigrationRelease.sol

The MigrationRelease contract manages the release of migrated tokens to eligible users based on Merkle proofs.
Expand All @@ -71,27 +95,34 @@ The MigrationRelease contract manages the release of migrated tokens to eligible
- Fair and transparent distribution mechanism
- Fund recovery safety mechanism

## Important Constants
## Important Constants and Params

- `VESTING_PERIOD`: 90 days
- `INSTANT_RATIO`: 75 (interpreted as 7.5x)
- `VESTING_RATIO`: 75 (interpreted as 7.5x)

**Release Model:**
- **Instant Release**: 50% of the locked amount is immediately available
- **Vested Release**: Additional 50% of the locked amount is available after a 90-day vesting period
- Total migration ratio: 1:15 (locked:received)
- **Instant Release**:
a. As users provide proof of their fund-lock, 50% of the locked amount is immediately released.
b. Once released, we record the timestamp of instant release.
c. Only after 90 days of this timestamp, users can unlock their vested release.

- **Vested Release**:
a. Vested release is the release that takes place 90 days after instant release.
b. Merkle proof is still required but the 90 days check is additionally imposed.

**Main Functions:**
- `releaseInstant(address _recipient, uint _amount, uint _id, bytes32[] calldata _merkleProof)`: Claims instant portion
- `releaseVested(address _recipient, uint _amount, uint _id)`: Claims vested portion after vesting period
- `releaseInstant(address _recipient, uint _amount, uint _epoch, bytes32[] calldata _merkleProof)`: Claims instant portion
- `releaseVested(address _recipient, uint _amount, uint _epoch)`: Claims vested portion after vesting period
- `setMerkleRoot(bytes32 _merkleRoot)`: Updates the Merkle root for verification
- `addFunds()`: Adds funds to the contract for distribution
- `pause()`: Pauses the contract to prevent claims
- `unpause()`: Unpauses the contract to allow claims
- `recoverFunds(address _token, address _to, uint _amount)`: Emergency function to recover funds

**Events:**
- `ReleasedInstant(address indexed recipient, uint indexed amount, uint indexed releaseTime)`
- `ReleasedVested(address indexed recipient, uint indexed amount, uint indexed releaseTime)`
- `ReleasedInstant(address indexed recipient, uint indexed amount, uint indexed epochId)`
- `ReleasedVested(address indexed recipient, uint indexed amount, uint indexed epochId)`
- `FundsAdded(uint indexed amount, uint indexed timestamp)`
- `MerkleRootUpdated(bytes32 indexed oldMerkleRoot, bytes32 indexed newMerkleRoot)`

Expand All @@ -102,18 +133,25 @@ The system uses a Merkle Tree for efficient and secure verification of eligible
### Merkle Tree Generation Process

1. Events are collected from the MigrationLocker contract using `fetchAndStoreEvents.js`
2. Each lock event produces a leaf in the Merkle Tree with `(address, amount, id)` as parameters
2. Each lock event produces a leaf in the Merkle Tree with `(address, amount, epochId)` as parameters
3. The Merkle root is calculated and set in the MigrationRelease contract
4. Users can provide proofs to verify their eligibility when claiming tokens

### Technical Implementation

The Merkle Tree implementation in `script/utils/merkle.js` provides these key functions:
The utility scripts in the `script/utils` folder handle the Merkle tree generation process:

- **merkle.js**: Core Merkle tree implementation with functions to hash leaves, generate roots, create proofs, and verify claims using the (address, amount, epoch) format.
- **fetchAndStoreEvents.js**: Fetches all "Locked" events from the MigrationLocker contract, groups them by address and epoch, combines amounts for duplicate addresses within the same epoch, and saves the processed claims.
- **getRoot.js**: Simple utility that loads processed claims and generates the Merkle root for deployment to the MigrationRelease contract.
- **config.js**: Contains configuration settings for contract addresses, ABIs, and file paths used by the utility scripts.

Key functions in `merkle.js`:

- `hashLeaf(address, amount, id)`: Creates hashed leaves for the Merkle Tree
- `hashLeaf(address, amount, epochId)`: Creates hashed leaves for the Merkle Tree
- `getRoot(claims)`: Generates the Merkle root from an array of claims
- `getProof(address, amount, id, claims)`: Generates a Merkle proof for a specific claim
- `verify(address, amount, id, claims)`: Verifies a claim against the Merkle Tree
- `getProof(address, amount, epochId, claims)`: Generates a Merkle proof for a specific claim
- `verify(address, amount, epochId, claims)`: Verifies a claim against the Merkle Tree

## Security Considerations

Expand All @@ -123,37 +161,26 @@ The system uses the following security measures for claims verification:

1. **Double-claim prevention**: Both instant and vested claims track their status in mappings
2. **Tamper-proof verification**: Merkle Tree verification ensures users can only claim their allocated amounts
3. **Parameter binding**: The address, amount, and ID must all match the Merkle proof
3. **Parameter binding**: The address, amount, and epochId must all match the Merkle proof
4. **Contract locking**: MigrationLocker can be locked to prevent new tokens from being locked

### Access Control

- Both contracts use OpenZeppelin's `Ownable2StepUpgradeable` for secure ownership management
- Critical functions are protected with `onlyOwner` modifier
- The MigrationLocker can be toggled between locked and unlocked states
- Both contracts can be paused/unpaused by owners to control operations

### Deployment Scripts

- `script/Deployments/DeployLocker.js`: Deploys the MigrationLocker contract
- `script/Deployments/DeployRelease.js`: Deploys the MigrationRelease contract and sets the Merkle root

## Utility Scripts

The project includes several utility scripts for managing the migration process:

- `script/utils/fetchAndStoreEvents.js`: Fetches lock events from the MigrationLocker contract
- `script/utils/merkle.js`: Contains functions for Merkle Tree generation and verification
- `script/utils/getRoot.js`: Computes the Merkle root from claims data
- `script/utils/getPoof.js`: Generates proofs for individual claims
- `script/utils/verify.js`: Verifies claims against the Merkle Tree
- `script/utils/proofArray.js`: Generates proofs for multiple claims
- `script/deploy/DeployLocker.s.sol`: Deploys the MigrationLocker contract
- `script/deploy/DeployRelease.s.sol`: Deploys the MigrationRelease contract and sets the Merkle root

## Usage Instructions

### Building the Project

```shell
npx hardhat compile
forge build
```

### Testing the Project
Expand Down
8 changes: 4 additions & 4 deletions src/MigrationRelease.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MigrationRelease is Initializable, Ownable2StepUpgradeable, PausableUpgradeable {
using SafeERC20 for IERC20;

event ReleasedInstant(address indexed recipient, uint256 indexed amount, uint256 indexed releaseTime);
event ReleasedVested(address indexed recipient, uint256 indexed amount, uint256 indexed releaseTime);
event ReleasedInstant(address indexed recipient, uint256 indexed amount, uint256 indexed epochId);
event ReleasedVested(address indexed recipient, uint256 indexed amount, uint256 indexed epochId);
event FundsAdded(uint256 indexed amount, uint256 indexed timestamp);

event MerkleRootUpdated(bytes32 indexed oldMerkleRoot, bytes32 indexed newMerkleRoot);
Expand Down Expand Up @@ -103,7 +103,7 @@ contract MigrationRelease is Initializable, Ownable2StepUpgradeable, PausableUpg

instantClaimTime[leaf] = block.timestamp;
totalReleased += instantAmount;
emit ReleasedInstant(_recipient, instantAmount, block.timestamp);
emit ReleasedInstant(_recipient, instantAmount, _epoch);

transferFunds(_recipient, instantAmount);
}
Expand Down Expand Up @@ -132,7 +132,7 @@ contract MigrationRelease is Initializable, Ownable2StepUpgradeable, PausableUpg
uint256 vestedAmount = (_amount * VESTING_RATIO) / 10; // Vested amount is 7.5 times the amount
claimedvested[leaf] = true;
totalReleased += vestedAmount;
emit ReleasedVested(_recipient, vestedAmount, block.timestamp);
emit ReleasedVested(_recipient, vestedAmount, _epoch);
transferFunds(_recipient, vestedAmount);
}

Expand Down
3 changes: 2 additions & 1 deletion test/MigrationLocker.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ contract MigrationLockerTest is Test {
}

function testCannotReinitialize() public {
vm.expectRevert("InvalidInitialization()");
vm.expectRevert(abi.encodeWithSelector(InvalidInitialization.selector));
locker.initialize(address(this));
}

Expand Down Expand Up @@ -414,4 +414,5 @@ contract MockMigrationLocker is Initializable, Ownable2StepUpgradeable, Pausable
}

error OwnableUnauthorizedAccount(address account);
error InvalidInitialization();
error EnforcedPause();
2 changes: 1 addition & 1 deletion test/MigrationRelease.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ contract MigrationReleaseTest is Test {
uint256 instantAmount = (CLAIM_AMOUNT_1 * release.INSTANT_RATIO()) / 10;

vm.expectEmit(true, true, true, true);
emit ReleasedVested(user1, expectedAmount, block.timestamp);
emit ReleasedVested(user1, expectedAmount, EPOCH);
release.releaseVested(user1, CLAIM_AMOUNT_1, EPOCH);

uint256 userBalanceAfter = user1.balance;
Expand Down