Skip to content

Commit

Permalink
refactor and test: ability for L2TargetDispenser to migrate
Browse files Browse the repository at this point in the history
  • Loading branch information
kupermind committed May 21, 2024
1 parent 0001113 commit f1aa0a6
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 16 deletions.
2 changes: 1 addition & 1 deletion contracts/Dispenser.sol
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ contract Dispenser {
bytes32[] memory updatedStakingTargets = new bytes32[](numActualTargets);
uint256[] memory updatedStakingAmounts = new uint256[](numActualTargets);
uint256 numPos;
for (uint256 j = 0; j < stakingTargets[j].length; ++j) {
for (uint256 j = 0; j < stakingTargets[i].length; ++j) {
if (positions[j]) {
updatedStakingTargets[numPos] = stakingTargets[i][j];
updatedStakingAmounts[numPos] = stakingIncentives[i][j];
Expand Down
3 changes: 2 additions & 1 deletion contracts/Tokenomics.sol
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ struct StakingPoint {
uint8 stakingFraction;
}

/// @title Tokenomics - Smart contract for tokenomics logic with incentives for unit owners and discount factor regulations for bonds.
/// @title Tokenomics - Smart contract for tokenomics logic with incentives for unit owners, discount factor
/// regulations for bonds, and staking incentives.
/// @author Aleksandr Kuperman - <[email protected]>
/// @author Andrey Lebedev - <[email protected]>
/// @author Mariapia Moscatiello - <[email protected]>
Expand Down
8 changes: 7 additions & 1 deletion contracts/interfaces/IBridgeErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ interface IBridgeErrors {
/// @dev The contract is paused.
error Paused();

// Reentrancy guard
/// @dev The contract is unpaused.
error Unpaused();

// @dev Reentrancy guard.
error ReentrancyGuard();

/// @dev The contract has migrated.
error ContractMigrated();
}
2 changes: 1 addition & 1 deletion contracts/staking/DefaultDepositProcessorL1.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "hardhat/console.sol";
import {IBridgeErrors} from "../interfaces/IBridgeErrors.sol";

interface IDispenser {
Expand Down
55 changes: 55 additions & 0 deletions contracts/staking/DefaultTargetDispenserL2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ interface IToken {
/// @param amount Token amount.
/// @return True if the function execution is successful.
function approve(address spender, uint256 amount) external returns (bool);

/// @dev Transfers the token amount.
/// @param to Address to transfer to.
/// @param amount The amount to transfer.
/// @return True if the function execution is successful.
function transfer(address to, uint256 amount) external returns (bool);
}

/// @title DefaultTargetDispenserL2 - Smart contract for processing tokens and data received on L2, and data sent back to L1.
Expand All @@ -50,6 +56,7 @@ abstract contract DefaultTargetDispenserL2 is IBridgeErrors {
event Drain(address indexed owner, uint256 amount);
event TargetDispenserPaused();
event TargetDispenserUnpaused();
event Migrated(address indexed sender, address indexed newL2TargetDispenser, uint256 amount);

// receiveMessage selector (Ethereum chain)
bytes4 public constant RECEIVE_MESSAGE = bytes4(keccak256(bytes("receiveMessage(bytes)")));
Expand Down Expand Up @@ -133,6 +140,11 @@ abstract contract DefaultTargetDispenserL2 is IBridgeErrors {
}
_locked = 2;

// Check that the contract is not migrated
if (owner == address(0)) {
revert ContractMigrated();
}

// Decode received data
(address[] memory targets, uint256[] memory amounts) = abi.decode(data, (address[], uint256[]));

Expand Down Expand Up @@ -393,6 +405,49 @@ abstract contract DefaultTargetDispenserL2 is IBridgeErrors {
_locked = 1;
}

/// @dev Migrates funds to a new specified L2 target dispenser contract address.
/// @notice The contract must be paused to prevent other interactions.
/// @notice Owner will be zeroed and the contract becomes paused for good.
function migrate(address newL2TargetDispenser) external {
// Reentrancy guard
if (_locked > 1) {
revert ReentrancyGuard();
}
_locked = 2;

// Check for the owner address
if (msg.sender != owner) {
revert OwnerOnly(msg.sender, owner);
}

// Check that the contract is paused
if (paused == 1) {
revert Unpaused();
}

// Check that the migration address is a contract
if (newL2TargetDispenser.code.length == 0) {
revert ZeroValue();
}

// Get OLAS token amount
uint256 amount = IToken(olas).balanceOf(address(this));
// Transfer amount to the new L2 target dispenser
if (amount > 0) {
bool success = IToken(olas).transfer(newL2TargetDispenser, amount);
if (!success) {
revert TransferFailed(olas, address(this), newL2TargetDispenser, amount);
}
}

// Zero the owner
owner = address(0);

emit Migrated(msg.sender, newL2TargetDispenser, amount);

// _locked is now set to 2 for good
}

/// @dev Receives native network token.
receive() external payable {
emit FundsReceived(msg.sender, msg.value);
Expand Down
19 changes: 17 additions & 2 deletions contracts/staking/GnosisTargetDispenserL2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ interface IBridge {
/// @author Andrey Lebedev - <[email protected]>
/// @author Mariapia Moscatiello - <[email protected]>
contract GnosisTargetDispenserL2 is DefaultTargetDispenserL2 {
// Bridge payload length
uint256 public constant BRIDGE_PAYLOAD_LENGTH = 32;
// L2 token relayer address
address public immutable l2TokenRelayer;

Expand Down Expand Up @@ -53,12 +55,25 @@ contract GnosisTargetDispenserL2 is DefaultTargetDispenserL2 {
}

/// @inheritdoc DefaultTargetDispenserL2
function _sendMessage(uint256 amount, bytes memory) internal override {
function _sendMessage(uint256 amount, bytes memory bridgePayload) internal override {
// Check for the bridge payload length
if (bridgePayload.length != BRIDGE_PAYLOAD_LENGTH) {
revert IncorrectDataLength(BRIDGE_PAYLOAD_LENGTH, bridgePayload.length);
}

// Get the gas limit from the bridge payload
uint256 gasLimitMessage = abi.decode(bridgePayload, (uint256));

// Check the gas limit value
if (gasLimitMessage < GAS_LIMIT) {
gasLimitMessage = GAS_LIMIT;
}

// Assemble AMB data payload
bytes memory data = abi.encodeWithSelector(RECEIVE_MESSAGE, abi.encode(amount));

// Send message to L1
bytes32 iMsg = IBridge(l2MessageRelayer).requireToPassMessage(l1DepositProcessor, data, GAS_LIMIT);
bytes32 iMsg = IBridge(l2MessageRelayer).requireToPassMessage(l1DepositProcessor, data, gasLimitMessage);

emit MessagePosted(uint256(iMsg), msg.sender, l1DepositProcessor, amount);
}
Expand Down
13 changes: 9 additions & 4 deletions contracts/staking/OptimismTargetDispenserL2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ contract OptimismTargetDispenserL2 is DefaultTargetDispenserL2 {
revert IncorrectDataLength(BRIDGE_PAYLOAD_LENGTH, bridgePayload.length);
}

// Send message to L1
// Reference: https://docs.optimism.io/builders/app-developers/bridging/messaging#for-l1-to-l2-transactions-1
uint256 cost = abi.decode(bridgePayload, (uint256));
// Extract bridge cost and gas limit from the bridge payload
(uint256 cost, uint256 gasLimitMessage) = abi.decode(bridgePayload, (uint256, uint256));

// Check for zero value
if (cost == 0) {
Expand All @@ -73,11 +72,17 @@ contract OptimismTargetDispenserL2 is DefaultTargetDispenserL2 {
revert LowerThan(msg.value, cost);
}

// Check the gas limit value
if (gasLimitMessage < GAS_LIMIT) {
gasLimitMessage = GAS_LIMIT;
}

// Assemble data payload
bytes memory data = abi.encodeWithSelector(RECEIVE_MESSAGE, abi.encode(amount));

// Send the message to L1 deposit processor
IBridge(l2MessageRelayer).sendMessage{value: cost}(l1DepositProcessor, data, uint32(GAS_LIMIT));
// Reference: https://docs.optimism.io/builders/app-developers/bridging/messaging#for-l1-to-l2-transactions-1
IBridge(l2MessageRelayer).sendMessage{value: cost}(l1DepositProcessor, data, uint32(gasLimitMessage));

emit MessagePosted(0, msg.sender, l1DepositProcessor, amount);
}
Expand Down
12 changes: 9 additions & 3 deletions contracts/staking/WormholeTargetDispenserL2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,20 @@ contract WormholeTargetDispenserL2 is DefaultTargetDispenserL2, TokenReceiver {
revert IncorrectDataLength(BRIDGE_PAYLOAD_LENGTH, bridgePayload.length);
}

address refundAccount = abi.decode(bridgePayload, (address));
// Extract refundAccount and gasLimitMessage from bridgePayload
(address refundAccount, uint256 gasLimitMessage) = abi.decode(bridgePayload, (address, uint256));
// If refundAccount is zero, default to msg.sender
if (refundAccount == address(0)) {
refundAccount = msg.sender;
}

// Check the gas limit value
if (gasLimitMessage < GAS_LIMIT) {
gasLimitMessage = GAS_LIMIT;
}

// Get a quote for the cost of gas for delivery
(uint256 cost, ) = IBridge(l2MessageRelayer).quoteEVMDeliveryPrice(uint16(l1SourceChainId), 0, GAS_LIMIT);
(uint256 cost, ) = IBridge(l2MessageRelayer).quoteEVMDeliveryPrice(uint16(l1SourceChainId), 0, gasLimitMessage);

// Check that provided msg.value is enough to cover the cost
if (cost > msg.value) {
Expand All @@ -108,7 +114,7 @@ contract WormholeTargetDispenserL2 is DefaultTargetDispenserL2, TokenReceiver {

// Send the message to L1
uint64 sequence = IBridge(l2MessageRelayer).sendPayloadToEvm{value: cost}(uint16(l1SourceChainId),
l1DepositProcessor, abi.encode(amount), 0, GAS_LIMIT, uint16(l1SourceChainId), refundAccount);
l1DepositProcessor, abi.encode(amount), 0, gasLimitMessage, uint16(l1SourceChainId), refundAccount);

emit MessagePosted(sequence, msg.sender, l1DepositProcessor, amount);
}
Expand Down
2 changes: 1 addition & 1 deletion test/DispenserDevIncentives.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { ethers } = require("hardhat");
const { expect } = require("chai");
const helpers = require("@nomicfoundation/hardhat-network-helpers");

describe.only("DispenserDevIncentives", async () => {
describe("DispenserDevIncentives", async () => {
const initialMint = "1" + "0".repeat(26);
const AddressZero = ethers.constants.AddressZero;
const HashZero = ethers.constants.HashZero;
Expand Down
101 changes: 100 additions & 1 deletion test/DispenserStakingIncentives.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { ethers } = require("hardhat");
const { expect } = require("chai");
const helpers = require("@nomicfoundation/hardhat-network-helpers");

describe.only("DispenserStakingIncentives", async () => {
describe("DispenserStakingIncentives", async () => {
const initialMint = "1" + "0".repeat(26);
const AddressZero = ethers.constants.AddressZero;
const HashZero = ethers.constants.HashZero;
Expand Down Expand Up @@ -879,5 +879,104 @@ describe.only("DispenserStakingIncentives", async () => {
// Restore to the state of the snapshot
await snapshot.restore();
});

it("Claim staking incentives for a single nominee with cross-bridging and withheld amount batch", async () => {
// Take a snapshot of the current state of the blockchain
const snapshot = await helpers.takeSnapshot();

// Set staking fraction to 100%
await tokenomics.changeIncentiveFractions(0, 0, 0, 0, 0, 100);
// Changing staking parameters
await tokenomics.changeStakingParams(100, 10);

// Checkpoint to apply changes
await helpers.time.increase(epochLen);
await tokenomics.checkpoint();

// Unpause the dispenser
await dispenser.setPauseState(0);

// Set gnosis deposit processor
await dispenser.setDepositProcessorChainIds([gnosisDepositProcessorL1.address], [gnosisChainId]);

// Add a non-whitelisted staking instance as a nominee
await vw.addNominee(deployer.address, gnosisChainId);
// Add a proxy instance as a nominee
await vw.addNominee(stakingInstance.address, gnosisChainId);

// Vote for nominees
await vw.setNomineeRelativeWeight(deployer.address, gnosisChainId, defaultWeight);
await vw.setNomineeRelativeWeight(stakingInstance.address, gnosisChainId, defaultWeight);

// Changing staking parameters for the next epoch
await tokenomics.changeStakingParams(50, 10);

// Checkpoint to account for weights
await helpers.time.increase(epochLen);
await tokenomics.checkpoint();

let stakingTargets;
// Setting targets in correct order
if (deployer.address.toString() < stakingInstance.address) {
stakingTargets = [convertAddressToBytes32(deployer.address), convertAddressToBytes32(stakingInstance.address)];
} else {
stakingTargets = [convertAddressToBytes32(stakingInstance.address), convertAddressToBytes32(deployer.address)];
}

let gnosisBridgePayload = ethers.utils.defaultAbiCoder.encode(["uint256"], [defaultGasLimit]);

// Claim staking incentives with the unverified target
await dispenser.claimStakingIncentivesBatch(numClaimedEpochs, [gnosisChainId], [stakingTargets],
[gnosisBridgePayload], [0]);

// Check that the target contract got OLAS
expect(await gnosisTargetDispenserL2.withheldAmount()).to.gt(0);

// Sync back the withheld amount
await gnosisTargetDispenserL2.syncWithheldTokens(bridgePayload);

// Get another staking instance
const MockStakingProxy = await ethers.getContractFactory("MockStakingProxy");
const stakingInstance2 = await MockStakingProxy.deploy(olas.address);
await stakingInstance2.deployed();

// Add a default implementation mocked as a proxy address itself
await stakingProxyFactory.addImplementation(stakingInstance2.address, stakingInstance2.address);

// Add a valid staking target nominee
await vw.addNominee(stakingInstance2.address, gnosisChainId);

// Setting targets in correct order
if (stakingInstance2.address.toString() < stakingInstance.address) {
stakingTargets = [convertAddressToBytes32(stakingInstance2.address), convertAddressToBytes32(stakingInstance.address)];
} else {
stakingTargets = [convertAddressToBytes32(stakingInstance.address), convertAddressToBytes32(stakingInstance2.address)];
}

// Set weights to a nominee
await vw.setNomineeRelativeWeight(stakingInstance2.address, gnosisChainId, defaultWeight);

// Changing staking parameters for the next epoch
await tokenomics.changeStakingParams(100, 10);

// Checkpoint to start the new epoch and able to claim
await helpers.time.increase(epochLen);
await tokenomics.checkpoint();

// Claim with withheld amount being accounted for
await dispenser.claimStakingIncentivesBatch(numClaimedEpochs, [gnosisChainId], [stakingTargets],
[gnosisBridgePayload], [0]);

// Checkpoint to start the new epoch and able to claim
await helpers.time.increase(epochLen);
await tokenomics.checkpoint();

// Claim again with withheld amount being accounted for
await dispenser.claimStakingIncentivesBatch(numClaimedEpochs, [gnosisChainId], [stakingTargets],
[gnosisBridgePayload], [0]);

// Restore to the state of the snapshot
await snapshot.restore();
});
});
});
2 changes: 1 addition & 1 deletion test/StakingBridging.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const { ethers } = require("hardhat");
const { expect } = require("chai");

describe.only("StakingBridging", async () => {
describe("StakingBridging", async () => {
const initialMint = "1" + "0".repeat(26);
const defaultDeposit = "1" + "0".repeat(22);
const AddressZero = ethers.constants.AddressZero;
Expand Down

0 comments on commit f1aa0a6

Please sign in to comment.