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

refactor(NODLMigration): prepare to create GrantsMigration #45

Merged
merged 2 commits into from
Jul 24, 2024
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
191 changes: 191 additions & 0 deletions src/bridge/BridgeBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity 0.8.23;

import {NODL} from "../NODL.sol";

/// @title BridgeBase
/// @notice Abstract contract for bridging free or vested tokens with the help of oracles.
/// @dev This contract provides basic functionalities for voting on proposals
/// to bridge tokens and ensuring certain constraints such as voting thresholds and delays.
abstract contract BridgeBase {
/// @notice Token contract address for the NODL token.
NODL public nodl;

/// @notice Mapping to track whether an address is an oracle.
mapping(address => bool) public isOracle;

/// @notice Minimum number of votes needed to execute a proposal.
uint8 public threshold;

/// @notice Number of blocks to delay before a proposal can be executed after reaching the voting threshold.
uint256 public delay;

/// @notice Maximum number of oracles allowed.
uint8 public constant MAX_ORACLES = 10;

/// @notice Mapping of oracles to proposals to track which oracle has voted on which proposal.
mapping(address => mapping(bytes32 => bool)) public voted;

/// @notice Emitted when the first vote a proposal has been cast.
event VoteStarted(bytes32 indexed proposal, address oracle, address indexed user, uint256 amount);

/// @notice Emitted when an oracle votes on a proposal which is already created.
event Voted(bytes32 indexed proposal, address oracle);

/// @notice Error to indicate an oracle has already voted on a proposal.
error AlreadyVoted(bytes32 proposal, address oracle);

/// @notice Error to indicate a proposal has already been executed.
error AlreadyExecuted(bytes32 proposal);

/// @notice Error to indicate parameters of a proposal have been changed after initiation.
error ParametersChanged(bytes32 proposal);

/// @notice Error to indicate an address is not recognized as an oracle.
error NotAnOracle(address user);

/// @notice Error to indicate it's too soon to execute a proposal.
/// Please note `withdraw` here refers to a function for free tokens where they are minted for the user.
/// We have kept the name for API compatibility.
error NotYetWithdrawable(bytes32 proposal);

/// @notice Error to indicate insufficient votes to execute a proposal.
error NotEnoughVotes(bytes32 proposal);

/// @notice Error to indicate that the number of oracles exceeds the allowed maximum.
error MaxOraclesExceeded();

/// @notice Initializes the contract with specified parameters.
/// @param bridgeOracles Array of oracle accounts.
/// @param token Contract address of the NODL token.
/// @param minVotes Minimum required votes to consider a proposal valid.
/// @param minDelay Blocks to wait before a passed proposal can be executed.
constructor(address[] memory bridgeOracles, NODL token, uint8 minVotes, uint256 minDelay) {
require(bridgeOracles.length >= minVotes, "Not enough oracles");
require(minVotes > 0, "Votes must be more than zero");

_mustNotExceedMaxOracles(bridgeOracles.length);

for (uint256 i = 0; i < bridgeOracles.length; i++) {
isOracle[bridgeOracles[i]] = true;
}

nodl = token;
threshold = minVotes;
delay = minDelay;
}

/// @notice Creates a new vote on a proposal.
/// @dev Sets initial values and emits a VoteStarted event.
/// @param proposal The hash identifier of the proposal.
/// @param oracle The oracle address initiating the vote.
/// @param user The user address associated with the vote.
/// @param amount The amount of tokens being bridged.
function _createVote(bytes32 proposal, address oracle, address user, uint256 amount) internal virtual {
_mustNotHaveVotedYet(proposal, oracle);

voted[oracle][proposal] = true;
_incTotalVotes(proposal);
_updateLastVote(proposal, block.number);
emit VoteStarted(proposal, oracle, user, amount);
}

/// @notice Records a vote for a proposal by an oracle.
/// @param proposal The hash identifier of the proposal.
/// @param oracle The oracle casting the vote.
function _recordVote(bytes32 proposal, address oracle) internal virtual {
_mustNotHaveVotedYet(proposal, oracle);

voted[oracle][proposal] = true;
_incTotalVotes(proposal);
_updateLastVote(proposal, block.number);
emit Voted(proposal, oracle);
}

/// @notice Executes a proposal after all conditions are met.
/// @param proposal The hash identifier of the proposal.
function _execute(bytes32 proposal) internal {
_mustNotHaveExecutedYet(proposal);
_mustHaveEnoughVotes(proposal);
_mustBePastSafetyDelay(proposal);

_flagAsExecuted(proposal);
}

/**
* @dev Updates the last vote value for a given proposal.
* @param proposal The identifier of the proposal.
* @param value The new value for the last vote.
*/
function _updateLastVote(bytes32 proposal, uint256 value) internal virtual;

/**
* @dev Increments the total votes count for a given proposal.
* @param proposal The identifier of the proposal.
*/
function _incTotalVotes(bytes32 proposal) internal virtual;

/**
* @dev Flags a proposal as executed.
* @param proposal The identifier of the proposal.
*/
function _flagAsExecuted(bytes32 proposal) internal virtual;

/**
* @dev Retrieves the last vote value for a given proposal.
* @param proposal The identifier of the proposal.
* @return The last vote value.
*/
function _lastVote(bytes32 proposal) internal view virtual returns (uint256);

/**
* @dev Retrieves the total votes count for a given proposal.
* @param proposal The identifier of the proposal.
* @return The total votes count.
*/
function _totalVotes(bytes32 proposal) internal view virtual returns (uint8);

/**
* @dev Checks if a proposal has been executed.
* @param proposal The identifier of the proposal.
* @return A boolean indicating if the proposal has been executed.
*/
function _executed(bytes32 proposal) internal view virtual returns (bool);

function _mustNotHaveExecutedYet(bytes32 proposal) internal view {
if (_executed(proposal)) {
revert AlreadyExecuted(proposal);
}
}

function _mustBePastSafetyDelay(bytes32 proposal) internal view {
if (block.number - _lastVote(proposal) < delay) {
revert NotYetWithdrawable(proposal);
}
}

function _mustHaveEnoughVotes(bytes32 proposal) internal view {
if (_totalVotes(proposal) < threshold) {
revert NotEnoughVotes(proposal);
}
}

function _mustBeAnOracle(address maybeOracle) internal view {
if (!isOracle[maybeOracle]) {
revert NotAnOracle(maybeOracle);
}
}

function _mustNotExceedMaxOracles(uint256 length) internal pure {
if (length > MAX_ORACLES) {
revert MaxOraclesExceeded();
}
}

function _mustNotHaveVotedYet(bytes32 proposal, address oracle) internal view {
if (voted[oracle][proposal]) {
revert AlreadyVoted(proposal, oracle);
}
}
}
111 changes: 30 additions & 81 deletions src/bridge/NODLMigration.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
pragma solidity 0.8.23;

import {NODL} from "../NODL.sol";
import {BridgeBase} from "./BridgeBase.sol";

/// @title NODLMigration
/// @notice This contract is used to help migrating the NODL assets from the Nodle Parachain
/// to the ZkSync contracts.
contract NODLMigration {
contract NODLMigration is BridgeBase {
struct Proposal {
address target;
uint256 amount;
Expand All @@ -16,43 +17,15 @@ contract NODLMigration {
bool executed;
}

NODL public nodl;
mapping(address => bool) public isOracle;
uint8 public threshold;
uint256 public delay;

// We track votes in a seperate mapping to avoid having to write helper functions to
// expose the votes for each proposal.
mapping(bytes32 => Proposal) public proposals;
mapping(address => mapping(bytes32 => bool)) public voted;

error AlreadyVoted(bytes32 proposal, address oracle);
error AlreadyExecuted(bytes32 proposal);
error ParametersChanged(bytes32 proposal);
error NotAnOracle(address user);
error NotYetWithdrawable(bytes32 proposal);
error NotEnoughVotes(bytes32 proposal);

event VoteStarted(bytes32 indexed proposal, address oracle, address indexed user, uint256 amount);
event Voted(bytes32 indexed proposal, address oracle);
event Withdrawn(bytes32 indexed proposal, address indexed user, uint256 amount);

/// @param bridgeOracles Array of oracle accounts that will be able to bridge the tokens.
/// @param token Contract address of the NODL token.
/// @param minVotes Minimum number of votes required to bridge the tokens. This needs to be
/// less than or equal to the number of oracles and is expected to be above 1.
/// @param minDelay Minimum delay in blocks before bridged tokens can be minted.
constructor(address[] memory bridgeOracles, NODL token, uint8 minVotes, uint256 minDelay) {
assert(bridgeOracles.length >= minVotes);
assert(minVotes > 0);

for (uint256 i = 0; i < bridgeOracles.length; i++) {
isOracle[bridgeOracles[i]] = true;
}
nodl = token;
threshold = minVotes;
delay = minDelay;
}
constructor(address[] memory bridgeOracles, NODL token, uint8 minVotes, uint256 minDelay)
BridgeBase(bridgeOracles, token, minVotes, minDelay)
{}

/// @notice Bridge some tokens from the Nodle Parachain to the ZkSync contracts. This
/// tracks "votes" from each oracle and unlocks execution after a withdrawal delay.
Expand All @@ -76,76 +49,52 @@ contract NODLMigration {
/// proposal has enough votes and has passed the safety delay.
/// @param paraTxHash The transaction hash on the Parachain for this transfer.
function withdraw(bytes32 paraTxHash) external {
_mustNotHaveExecutedYet(paraTxHash);
_mustHaveEnoughVotes(paraTxHash);
_mustBePastSafetyDelay(paraTxHash);

_execute(paraTxHash);
_withdraw(paraTxHash, proposals[paraTxHash].target, proposals[paraTxHash].amount);
}

function _mustBeAnOracle(address maybeOracle) internal view {
if (!isOracle[maybeOracle]) {
revert NotAnOracle(maybeOracle);
}
}

function _mustNotHaveVotedYet(bytes32 proposal, address oracle) internal view {
if (voted[oracle][proposal]) {
revert AlreadyVoted(proposal, oracle);
}
}

function _mustNotHaveExecutedYet(bytes32 proposal) internal view {
if (proposals[proposal].executed) {
revert AlreadyExecuted(proposal);
}
}

function _mustNotBeChangingParameters(bytes32 proposal, address user, uint256 amount) internal view {
if (proposals[proposal].amount != amount || proposals[proposal].target != user) {
revert ParametersChanged(proposal);
}
}

function _mustBePastSafetyDelay(bytes32 proposal) internal view {
if (block.number - proposals[proposal].lastVote < delay) {
revert NotYetWithdrawable(proposal);
}
}

function _mustHaveEnoughVotes(bytes32 proposal) internal view {
if (proposals[proposal].totalVotes < threshold) {
revert NotEnoughVotes(proposal);
}
}

function _proposalExists(bytes32 proposal) internal view returns (bool) {
return proposals[proposal].totalVotes > 0 && proposals[proposal].amount > 0;
}

function _createVote(bytes32 proposal, address oracle, address user, uint256 amount) internal {
voted[oracle][proposal] = true;
function _createVote(bytes32 proposal, address oracle, address user, uint256 amount) internal override {
proposals[proposal].target = user;
proposals[proposal].amount = amount;
proposals[proposal].totalVotes = 1;
proposals[proposal].lastVote = block.number;
super._createVote(proposal, oracle, user, amount);
}

function _withdraw(bytes32 proposal, address user, uint256 amount) internal {
nodl.mint(user, amount);
emit Withdrawn(proposal, user, amount);
}

emit VoteStarted(proposal, oracle, user, amount);
function _flagAsExecuted(bytes32 proposal) internal override {
proposals[proposal].executed = true;
}

function _recordVote(bytes32 proposal, address oracle) internal {
voted[oracle][proposal] = true;
// this is safe since we are unlikely to have maxUint8 oracles to manage
proposals[proposal].totalVotes += 1;
proposals[proposal].lastVote = block.number;
function _incTotalVotes(bytes32 proposal) internal override {
proposals[proposal].totalVotes++;
}

emit Voted(proposal, oracle);
function _updateLastVote(bytes32 proposal, uint256 value) internal override {
proposals[proposal].lastVote = value;
}

function _withdraw(bytes32 proposal, address user, uint256 amount) internal {
proposals[proposal].executed = true;
nodl.mint(user, amount);
function _totalVotes(bytes32 proposal) internal view override returns (uint8) {
return proposals[proposal].totalVotes;
}

emit Withdrawn(proposal, user, amount);
function _lastVote(bytes32 proposal) internal view override returns (uint256) {
return proposals[proposal].lastVote;
}

function _executed(bytes32 proposal) internal view override returns (bool) {
return proposals[proposal].executed;
}
}
Loading