Skip to content

Commit

Permalink
refactor(NODLMigration): prepare to create GrantsMigration (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
aliXsed authored Jul 24, 2024
1 parent 4554caa commit 90497b7
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 91 deletions.
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

0 comments on commit 90497b7

Please sign in to comment.