Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contracts/core): support migration to omni staking #2993

Closed
wants to merge 1 commit into from
Closed
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
6 changes: 3 additions & 3 deletions contracts/core/.gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ FeeOracleV2_Test:test_setProtocolFee() (gas: 32367)
FeeOracleV2_Test:test_setToNativeRate() (gas: 42821)
InitializableHelper_Test:test_disableInitalizers() (gas: 181686)
InitializableHelper_Test:test_getInitialized() (gas: 178023)
MerkleDistributorWithDeadline_Test:test_claim() (gas: 1663968)
MerkleDistributorWithDeadline_Test:test_migrateToOmni_reverts() (gas: 402073)
MerkleDistributorWithDeadline_Test:test_migrateToOmni_succeeds() (gas: 8090155)
MerkleDistributorWithDeadline_Test:test_claim() (gas: 1396224)
MerkleDistributorWithDeadline_Test:test_migrateToOmni_reverts() (gas: 403343)
MerkleDistributorWithDeadline_Test:test_migrateToOmni_succeeds() (gas: 9726691)
OmniBridgeL1_Test:test_bridge() (gas: 228802)
OmniBridgeL1_Test:test_initialize() (gas: 1749710)
OmniBridgeL1_Test:test_pauseAll() (gas: 49376)
Expand Down
17 changes: 11 additions & 6 deletions contracts/core/script/manual/GenesisStake.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ contract GenesisStakeScript is Script {
GenesisStake internal genesisStake;
MerkleDistributorWithDeadline internal merkleDistributor;

uint8 internal deployNum = 1; // Increment each deployment for new salts
address internal validator = 0xD6CD71dF91a6886f69761826A9C4D123178A8d9D;
uint256 internal endTime = block.timestamp + 30 days;
uint256 internal depositAmount = 80 ether;
uint256 internal depositAmount = 70 ether;
uint256 internal rewardAmount = 20 ether;
uint256 internal userAmount = 10 ether;
bytes32[] internal leaves = new bytes32[](2);
bytes32[][] internal proofs = new bytes32[][](2);
bytes32 internal root;
Expand Down Expand Up @@ -58,13 +61,14 @@ contract GenesisStakeScript is Script {
}

function _deployContracts() internal {
address genesisStakeAddr = create3.getDeployed(msg.sender, keccak256("genesisStake"));
address merkleDistributorAddr = create3.getDeployed(msg.sender, keccak256("merkleDistributor"));
address genesisStakeAddr = create3.getDeployed(msg.sender, keccak256(abi.encode("genesisStake", deployNum)));
address merkleDistributorAddr =
create3.getDeployed(msg.sender, keccak256(abi.encode("merkleDistributor", deployNum)));

address genesisStakeImpl = address(new GenesisStake(address(omni), merkleDistributorAddr));
genesisStake = GenesisStake(
create3.deploy(
keccak256("genesisStake"),
keccak256(abi.encode("genesisStake", deployNum)),
abi.encodePacked(
type(TransparentUpgradeableProxy).creationCode,
abi.encode(
Expand All @@ -75,7 +79,7 @@ contract GenesisStakeScript is Script {
);
merkleDistributor = MerkleDistributorWithDeadline(
create3.deploy(
keccak256("merkleDistributor"),
keccak256(abi.encode("merkleDistributor", deployNum)),
abi.encodePacked(
type(MerkleDistributorWithDeadline).creationCode,
abi.encode(address(omni), root, endTime, address(portal), genesisStakeAddr, address(inbox))
Expand All @@ -89,11 +93,12 @@ contract GenesisStakeScript is Script {

function _approveStakeAndFund() internal {
omni.approve(address(genesisStake), type(uint256).max);
omni.approve(address(merkleDistributor), type(uint256).max);
genesisStake.stake(depositAmount);
omni.transfer(address(merkleDistributor), rewardAmount);
}

function _migrateToOmni() internal {
merkleDistributor.migrateToOmni(0, rewardAmount, proofs[0]);
merkleDistributor.migrateToOmni(0, rewardAmount, userAmount, proofs[0], validator);
}
}
11 changes: 11 additions & 0 deletions contracts/core/src/interfaces/IStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.24;

interface IStaking {
/**
* @notice Delegate tokens to a validator for another address
* @param delegator The address of the delegator
* @param validator The address of the validator to delegate to
*/
function delegateFor(address delegator, address validator) external payable;
}
61 changes: 44 additions & 17 deletions contracts/core/src/token/MerkleDistributorWithDeadline.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Ownable } from "solady/src/auth/Ownable.sol";
import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol";
import { MerkleProofLib } from "solady/src/utils/MerkleProofLib.sol";
import { LibBitmap } from "solady/src/utils/LibBitmap.sol";
import { IStaking } from "../interfaces/IStaking.sol";
import { IOmniPortal } from "../interfaces/IOmniPortal.sol";
import { IGenesisStake } from "../interfaces/IGenesisStake.sol";
import { IERC7683, IOriginSettler } from "solve/src/erc7683/IOriginSettler.sol";
Expand All @@ -20,6 +21,7 @@ contract MerkleDistributorWithDeadline is MerkleDistributor, Ownable {
error ClaimWindowFinished();
error NoWithdrawDuringClaim();

address internal constant OMNI_STAKING = 0xCCcCcC0000000000000000000000000000000001;
bytes32 internal constant ORDERDATA_TYPEHASH = keccak256(
"OrderData(address owner,uint64 destChainId,Deposit deposit,Call[] calls,Expense[] expenses)Deposit(address token,uint96 amount)Call(address target,bytes4 selector,uint256 value,bytes params)Expense(address spender,address token,uint96 amount)"
);
Expand Down Expand Up @@ -65,34 +67,49 @@ contract MerkleDistributorWithDeadline is MerkleDistributor, Ownable {
* @notice Claim rewards and/or migrate stake to Omni
* @dev Triggers a SolverNet order to generate a subsidized order for deposited tokens on Omni 1:1
* If the user has already claimed rewards, they can still migrate their stake to Omni
* @param index Index of the claim
* @param amount Amount of tokens to claim
* @param merkleProof Merkle proof for the claim
* If validator is not set, the user's tokens will be delivered to their address instead of staked
* @param index Index of the claim
* @param claimAmount Amount of tokens to claim
* @param userAmount Amount of tokens to transfer from user to add to migration order
* @param merkleProof Merkle proof for the claim
* @param validator Validator to delegate to
*/
function migrateToOmni(uint256 index, uint256 amount, bytes32[] calldata merkleProof) external {
function migrateToOmni(
uint256 index,
uint256 claimAmount,
uint256 userAmount,
bytes32[] calldata merkleProof,
address validator
) external {
if (block.timestamp > endTime) revert ClaimWindowFinished();

// Migrate user's stake to Omni, if any
uint256 stake = IGenesisStake(genesisStaking).migrateStake(msg.sender);
// Retrieve user's staked tokens from GenesisStake
uint256 stakeAmount = IGenesisStake(genesisStaking).migrateStake(msg.sender);

// Mark reward distribution as claimed and add it to user's stake
if (!isClaimed(index)) {
if (!isClaimed(index) && merkleProof.length > 0) {
// Verify the merkle proof.
bytes32 node = keccak256(abi.encodePacked(index, msg.sender, amount));
bytes32 node = keccak256(abi.encodePacked(index, msg.sender, claimAmount));
if (!MerkleProofLib.verifyCalldata(merkleProof, merkleRoot, node)) revert InvalidProof();

// Update bitmap and add claim amount to stake amount
claimedBitMap.set(index);
unchecked {
stake += amount;
stakeAmount += claimAmount;
}
}

// Cannot migrate if user has no stake to migrate
if (stake == 0) revert NothingToMigrate();
// Transfer additional tokens from user, if any, to add to migration order
if (userAmount > 0) {
token.safeTransferFrom(msg.sender, address(this), userAmount);
stakeAmount += userAmount;
}

// Cannot migrate if user has no tokens to migrate
if (stakeAmount == 0) revert NothingToMigrate();

// Generate and send the order
IERC7683.OnchainCrossChainOrder memory order = _generateOrder(msg.sender, stake);
IERC7683.OnchainCrossChainOrder memory order = _generateOrder(msg.sender, stakeAmount, validator);
solvernetInbox.open(order);
}

Expand All @@ -103,19 +120,29 @@ contract MerkleDistributorWithDeadline is MerkleDistributor, Ownable {

/**
* @notice Generate a SolverNet order that generates a subsidized order for deposited tokens on Omni 1:1
* @param account Address of the user claiming
* @param amount Amount of tokens to claim
* @return SolverNet order
* @param account Address of the user claiming
* @param amount Amount of tokens to claim
* @param validator Validator to delegate to
* @return SolverNet order
*/
function _generateOrder(address account, uint256 amount)
function _generateOrder(address account, uint256 amount, address validator)
internal
view
returns (IERC7683.OnchainCrossChainOrder memory)
{
SolverNet.Deposit memory deposit = SolverNet.Deposit({ token: token, amount: uint96(amount) });

SolverNet.Call[] memory call = new SolverNet.Call[](1);
call[0] = SolverNet.Call({ target: account, selector: bytes4(0), value: amount, params: "" });
if (validator == address(0)) {
call[0] = SolverNet.Call({ target: account, selector: bytes4(0), value: amount, params: "" });
} else {
call[0] = SolverNet.Call({
target: OMNI_STAKING,
selector: IStaking.delegateFor.selector,
value: amount,
params: abi.encode(account, validator)
});
}

SolverNet.OrderData memory orderData = SolverNet.OrderData({
owner: account,
Expand Down
44 changes: 31 additions & 13 deletions contracts/core/test/token/MerkleDistributorWithDeadline.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GenesisStake } from "src/token/GenesisStake.sol";
import { MerkleDistributor } from "src/token/MerkleDistributor.sol";
import { MerkleDistributorWithDeadline } from "src/token/MerkleDistributorWithDeadline.sol";

import { IStaking } from "src/interfaces/IStaking.sol";
import { IERC7683, IOriginSettler } from "solve/src/erc7683/IOriginSettler.sol";
import { SolverNet } from "solve/src/lib/SolverNet.sol";

Expand All @@ -35,6 +36,7 @@ contract MerkleDistributorWithDeadline_Test is Test {
uint256 initialSupply = 1_000_000 ether;
uint256 addrCount = 32;

address internal constant OMNI_STAKING = 0xCCcCcC0000000000000000000000000000000001;
bytes32 internal constant ORDER_DATA_TYPEHASH = keccak256(
"OrderData(address owner,uint64 destChainId,Deposit deposit,Call[] calls,Expense[] expenses)Deposit(address token,uint96 amount)Call(address target,bytes4 selector,uint256 value,bytes params)Expense(address spender,address token,uint96 amount)"
);
Expand Down Expand Up @@ -127,6 +129,7 @@ contract MerkleDistributorWithDeadline_Test is Test {
// Fund stakers and the distributor contract
function _fundEverything() internal {
for (uint256 i; i < addrCount; ++i) {
if (i % 2 == 0) deal(address(omni), stakers[i], 100 ether);
omni.transfer(stakers[i], amounts[i]);
}

Expand All @@ -144,15 +147,26 @@ contract MerkleDistributorWithDeadline_Test is Test {
}

// Generate an ERC7683 order to check merkle distributor's call to inbox against
function generateERC7683Order(address addr, uint256 amount)
function generateERC7683Order(address addr, uint256 claimAmount, uint256 userAmount, address validator)
public
view
returns (IERC7683.OnchainCrossChainOrder memory)
{
SolverNet.Deposit memory deposit = SolverNet.Deposit({ token: address(omni), amount: uint96(amount * 2) });
SolverNet.Deposit memory deposit =
SolverNet.Deposit({ token: address(omni), amount: uint96((claimAmount * 2) + userAmount) });

SolverNet.Call[] memory call = new SolverNet.Call[](1);
call[0] = SolverNet.Call({ target: addr, selector: bytes4(0), value: amount * 2, params: "" });
if (validator == address(0)) {
call[0] =
SolverNet.Call({ target: addr, selector: bytes4(0), value: (claimAmount * 2) + userAmount, params: "" });
} else {
call[0] = SolverNet.Call({
target: OMNI_STAKING,
selector: IStaking.delegateFor.selector,
value: (claimAmount * 2) + userAmount,
params: abi.encode(addr, validator)
});
}

SolverNet.OrderData memory orderData = SolverNet.OrderData({
owner: addr,
Expand All @@ -174,7 +188,7 @@ contract MerkleDistributorWithDeadline_Test is Test {
for (uint256 i; i < addrCount; ++i) {
vm.prank(stakers[i]);
merkleDistributor.claim(i, stakers[i], amounts[i], proofs[i]);
assertEq(omni.balanceOf(stakers[i]), amounts[i]);
assertEq(omni.balanceOf(stakers[i]), amounts[i] + (i % 2 == 0 ? 100 ether : 0));
}
}

Expand All @@ -183,47 +197,51 @@ contract MerkleDistributorWithDeadline_Test is Test {
vm.warp(endTime + 1);
vm.prank(stakers[0]);
vm.expectRevert(MerkleDistributorWithDeadline.ClaimWindowFinished.selector);
merkleDistributor.migrateToOmni(0, amounts[0], proofs[0]);
merkleDistributor.migrateToOmni(0, amounts[0], 0, proofs[0], address(0));

// Cannot migrate if proof is invalid
bytes32 proof = proofs[0][0];
proofs[0][0] = bytes32(uint256(proof) + 1);
vm.warp(endTime - 1);
vm.prank(stakers[0]);
vm.expectRevert(MerkleDistributor.InvalidProof.selector);
merkleDistributor.migrateToOmni(0, amounts[0], proofs[0]);
merkleDistributor.migrateToOmni(0, amounts[0], 0, proofs[0], address(0));
proofs[0][0] = proof;

// Fully claim all stake and rewards
vm.prank(stakers[0]);
merkleDistributor.migrateToOmni(0, amounts[0], proofs[0]);
merkleDistributor.migrateToOmni(0, amounts[0], 0, proofs[0], address(0));

// Cannot migrate if user has no stake to migrate
vm.prank(stakers[0]);
vm.expectRevert(MerkleDistributorWithDeadline.NothingToMigrate.selector);
merkleDistributor.migrateToOmni(0, amounts[0], proofs[0]);
merkleDistributor.migrateToOmni(0, amounts[0], 0, proofs[0], address(0));
}

// Fully test migrateToOmni for all members of the merkle tree
function test_migrateToOmni_succeeds() public {
for (uint256 i; i < addrCount; ++i) {
vm.startPrank(stakers[i]);
uint256 inboxBalance = omni.balanceOf(address(inbox));
omni.approve(address(merkleDistributor), type(uint256).max);

// Get IERC7683 order and resolved orders
IERC7683.OnchainCrossChainOrder memory order = generateERC7683Order(stakers[i], amounts[i]);
IERC7683.OnchainCrossChainOrder memory order = generateERC7683Order(
stakers[i], amounts[i], i % 2 == 0 ? 100 ether : 0, i % 3 == 0 ? stakers[i] : address(0)
);
IERC7683.ResolvedCrossChainOrder memory resolved = inbox.resolve(order);

// Confirm merkleDistributor is calling the inbox with the order and that the resolved order is emitted
vm.expectCall(address(inbox), abi.encodeCall(MockSolverNetInbox.open, (order)));
vm.expectEmit(true, true, true, true);
emit IERC7683.Open(resolved.orderId, resolved);
merkleDistributor.migrateToOmni(i, amounts[i], proofs[i]);
merkleDistributor.migrateToOmni(
i, amounts[i], i % 2 == 0 ? 100 ether : 0, proofs[i], i % 3 == 0 ? stakers[i] : address(0)
);

// Confirm the inbox balance has increased by the user's staked balance and claim reward
assertEq(omni.balanceOf(address(inbox)), inboxBalance + amounts[i] * 2);
// Confirm the inbox balance has increased by the user's staked balance, claim reward, and user's additional tokens
assertEq(omni.balanceOf(address(inbox)), inboxBalance + amounts[i] * 2 + (i % 2 == 0 ? 100 ether : 0));
assertEq(omni.balanceOf(stakers[i]), 0);

vm.stopPrank();
}
}
Expand Down
Loading