Skip to content

Commit

Permalink
feat(GrantsMigration): allow permissioned off chain oracles to mint t…
Browse files Browse the repository at this point in the history
…okens and lock them for grants (#46)
  • Loading branch information
aliXsed authored Jul 25, 2024
1 parent 90497b7 commit d1c1049
Show file tree
Hide file tree
Showing 3 changed files with 379 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/bridge/BridgeBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {NODL} from "../NODL.sol";
/// 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;
NODL public immutable nodl;

/// @notice Mapping to track whether an address is an oracle.
mapping(address => bool) public isOracle;
Expand Down
159 changes: 159 additions & 0 deletions src/bridge/GrantsMigration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity 0.8.23;

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

contract GrantsMigration is BridgeBase {
/// @dev Represents the vesting details of a proposal.
struct Proposal {
address target; // Address of the grant recipient.
uint256 amount; // Total amount of tokens to be vested.
Grants.VestingSchedule[] schedules; // Array of vesting schedules.
}

/// @dev Tracks voting and execution status of a proposal.
struct ProposalStatus {
uint256 lastVote; // Timestamp of the last vote.
uint8 totalVotes; // Total number of votes cast.
bool executed; // Whether the proposal has been executed.
}

// State variables
Grants public immutable grants; // Grants contract.
mapping(bytes32 => Proposal) public proposals; // Proposals identified by a hash.
mapping(bytes32 => ProposalStatus) public proposalStatus; // Status of each proposal.

// Events
event Granted(bytes32 indexed proposal, address indexed user, uint256 amount, uint256 numOfSchedules);

/**
* @param bridgeOracles Array of addresses authorized to initiate and vote on proposals.
* @param token Address of the NODL token used for grants.
* @param _grants Address of the Grants contract managing vesting schedules.
* @param minVotes Minimum number of votes required to execute a proposal.
* @param minDelay Minimum delay before a proposal can be executed.
*/
constructor(address[] memory bridgeOracles, NODL token, Grants _grants, uint8 minVotes, uint256 minDelay)
BridgeBase(bridgeOracles, token, minVotes, minDelay)
{
grants = _grants;
}

/**
* @notice Bridges a proposal for grant distribution across chains or domains.
* @param paraTxHash Hash of the cross-chain transaction or parameter.
* @param user Recipient of the grant.
* @param amount Total token amount for the grant.
* @param schedules Array of VestingSchedule, detailing the vesting mechanics.
*/
function bridge(bytes32 paraTxHash, address user, uint256 amount, Grants.VestingSchedule[] memory schedules)
external
{
_mustBeAnOracle(msg.sender);
_mustNotHaveExecutedYet(paraTxHash);

if (_proposalExists(paraTxHash)) {
_mustNotHaveVotedYet(paraTxHash, msg.sender);
_mustNotBeChangingParameters(paraTxHash, user, amount, schedules);
_recordVote(paraTxHash, msg.sender);
} else {
_createProposal(paraTxHash, msg.sender, user, amount, schedules);
}
}

/**
* @notice Executes the grant proposal, transferring vested tokens according to the schedules.
* @param paraTxHash Hash of the proposal to be executed.
*/
function grant(bytes32 paraTxHash) external {
_execute(paraTxHash);
Proposal storage p = proposals[paraTxHash];
nodl.mint(address(this), proposals[paraTxHash].amount);
nodl.approve(address(grants), proposals[paraTxHash].amount);
for (uint256 i = 0; i < p.schedules.length; i++) {
grants.addVestingSchedule(
p.target,
p.schedules[i].start,
p.schedules[i].period,
p.schedules[i].periodCount,
p.schedules[i].perPeriodAmount,
p.schedules[i].cancelAuthority
);
}
emit Granted(paraTxHash, p.target, p.amount, p.schedules.length);
}

// Internal helper functions below

function _mustNotBeChangingParameters(
bytes32 proposal,
address user,
uint256 amount,
Grants.VestingSchedule[] memory schedules
) internal view {
Proposal storage storedProposal = proposals[proposal];

if (storedProposal.amount != amount || storedProposal.target != user) {
revert ParametersChanged(proposal);
}

uint256 len = storedProposal.schedules.length;
if (len != schedules.length) {
revert ParametersChanged(proposal);
}

for (uint256 i = 0; i < len; i++) {
if (
storedProposal.schedules[i].start != schedules[i].start
|| storedProposal.schedules[i].period != schedules[i].period
|| storedProposal.schedules[i].periodCount != schedules[i].periodCount
|| storedProposal.schedules[i].perPeriodAmount != schedules[i].perPeriodAmount
|| storedProposal.schedules[i].cancelAuthority != schedules[i].cancelAuthority
) {
revert ParametersChanged(proposal);
}
}
}

function _createProposal(
bytes32 proposal,
address oracle,
address user,
uint256 amount,
Grants.VestingSchedule[] memory schedules
) internal {
proposals[proposal] = Proposal({target: user, amount: amount, schedules: schedules});
super._createVote(proposal, oracle, user, amount);
}

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

function _flagAsExecuted(bytes32 proposal) internal override {
proposalStatus[proposal].executed = true;
}

function _incTotalVotes(bytes32 proposal) internal override {
proposalStatus[proposal].totalVotes++;
}

function _updateLastVote(bytes32 proposal, uint256 value) internal override {
proposalStatus[proposal].lastVote = value;
}

function _totalVotes(bytes32 proposal) internal view override returns (uint8) {
return proposalStatus[proposal].totalVotes;
}

function _lastVote(bytes32 proposal) internal view override returns (uint256) {
return proposalStatus[proposal].lastVote;
}

function _executed(bytes32 proposal) internal view override returns (bool) {
return proposalStatus[proposal].executed;
}
}
219 changes: 219 additions & 0 deletions test/bridge/GrantsMigration.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.23;

import {Test, console} from "forge-std/Test.sol";
import {GrantsMigration} from "../../src/bridge/GrantsMigration.sol";
import {BridgeBase} from "../../src/bridge/BridgeBase.sol";
import {Grants} from "../../src/Grants.sol";
import {NODL} from "../../src/NODL.sol";

contract GrantsMigrationTest is Test {
GrantsMigration migration;
Grants grants;
NODL nodl;

uint256 delay = 100;
address[] oracles = [vm.addr(1), vm.addr(2), vm.addr(3)];
address user = vm.addr(4);
Grants.VestingSchedule[] schedules;
uint256 amount = 0;

function setUp() public {
nodl = new NODL();
grants = new Grants(address(nodl));
migration = new GrantsMigration(oracles, nodl, grants, 2, delay);

schedules.push(
Grants.VestingSchedule({
start: block.timestamp + 1 days,
period: 1 days,
periodCount: 5,
perPeriodAmount: 20,
cancelAuthority: user
})
);
schedules.push(
Grants.VestingSchedule({
start: block.timestamp + 2 days,
period: 3 days,
periodCount: 7,
perPeriodAmount: 19,
cancelAuthority: oracles[0]
})
);
schedules.push(
Grants.VestingSchedule({
start: block.timestamp + 7 days,
period: 11 days,
periodCount: 13,
perPeriodAmount: 37,
cancelAuthority: address(0)
})
);

for (uint256 i = 0; i < schedules.length; i++) {
amount += schedules[i].perPeriodAmount * schedules[i].periodCount;
}

nodl.grantRole(nodl.MINTER_ROLE(), address(migration));
}

function test_oraclesAreRegisteredProperly() public {
for (uint256 i = 0; i < oracles.length; i++) {
assertEq(migration.isOracle(oracles[i]), true);
}
assertEq(migration.threshold(), 2);
assertEq(migration.delay(), delay);
}

function test_proposalCreationAndVoting() public {
bytes32 paraTxHash = keccak256(abi.encodePacked("tx1"));
vm.prank(oracles[0]);
vm.expectEmit();
emit BridgeBase.VoteStarted(paraTxHash, oracles[0], user, amount);
migration.bridge(paraTxHash, user, amount, schedules);

vm.prank(oracles[1]);
vm.expectEmit();
emit BridgeBase.Voted(paraTxHash, oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);

(uint256 lastVote, uint8 totalVotes, bool executed) = migration.proposalStatus(paraTxHash);
assertEq(totalVotes, 2);
assertEq(executed, false);
assertEq(lastVote, block.timestamp);
}

function test_executionOfProposals() public {
bytes32 paraTxHash = keccak256(abi.encodePacked("tx2"));
vm.prank(oracles[0]);
migration.bridge(paraTxHash, user, amount, schedules);

vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);

vm.roll(block.number + delay + 1);

vm.prank(oracles[0]);
vm.expectEmit();
emit GrantsMigration.Granted(paraTxHash, user, amount, schedules.length);
migration.grant(paraTxHash);

(,, bool executed) = migration.proposalStatus(paraTxHash);
assertEq(executed, true);
}

function test_proposalParameterChangesPreventVoting() public {
bytes32 paraTxHash = keccak256(abi.encodePacked("tx3"));
vm.prank(oracles[0]);
migration.bridge(paraTxHash, user, amount, schedules);

vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount + 1, schedules);

// Change the target address
vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, oracles[1], amount, schedules);

schedules[0].start += 1 days;
vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);
schedules[0].start -= 1 days;

schedules[1].period += 1 days;
vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);
schedules[1].period -= 1 days;

schedules[2].periodCount += 1;
vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);
schedules[2].periodCount -= 1;

schedules[0].perPeriodAmount += 1;
vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);
schedules[0].perPeriodAmount -= 1;

schedules[1].cancelAuthority = oracles[2];
vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);
schedules[1].cancelAuthority = oracles[0];

Grants.VestingSchedule[] memory newSchedules = new Grants.VestingSchedule[](schedules.length - 1);
newSchedules[0] = schedules[0];
newSchedules[1] = schedules[1];
vm.expectRevert(abi.encodeWithSelector(BridgeBase.ParametersChanged.selector, paraTxHash));
vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, newSchedules);

vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);
}

function test_rejectionOfDuplicateVotes() public {
bytes32 paraTxHash = keccak256(abi.encodePacked("tx4"));
vm.prank(oracles[0]);
migration.bridge(paraTxHash, user, amount, schedules);

vm.expectRevert(abi.encodeWithSelector(BridgeBase.AlreadyVoted.selector, paraTxHash, oracles[0]));
vm.prank(oracles[0]);
migration.bridge(paraTxHash, user, amount, schedules);
}

function test_rejectionOfVoteOnAlreadyExecuted() public {
bytes32 paraTxHash = keccak256(abi.encodePacked("tx5"));
vm.prank(oracles[0]);
migration.bridge(paraTxHash, user, amount, schedules);

vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, amount, schedules);

vm.roll(block.number + delay + 1);
migration.grant(paraTxHash);

vm.expectRevert(abi.encodeWithSelector(BridgeBase.AlreadyExecuted.selector, paraTxHash));
vm.prank(oracles[2]);
migration.bridge(paraTxHash, user, amount, schedules);
}

function test_executionFailsIfInsufficientVotes() public {
bytes32 paraTxHash = keccak256(abi.encodePacked("tx6"));
vm.prank(oracles[0]);
migration.bridge(paraTxHash, user, 100, schedules);

vm.roll(block.number + delay + 1);

vm.expectRevert(abi.encodeWithSelector(BridgeBase.NotEnoughVotes.selector, paraTxHash));
migration.grant(paraTxHash);
}

function test_executionFailsIfTooEarly() public {
bytes32 paraTxHash = keccak256(abi.encodePacked("tx7"));
vm.prank(oracles[0]);
migration.bridge(paraTxHash, user, 100, schedules);

vm.prank(oracles[1]);
migration.bridge(paraTxHash, user, 100, schedules);

vm.expectRevert(abi.encodeWithSelector(BridgeBase.NotYetWithdrawable.selector, paraTxHash));
migration.grant(paraTxHash);
}

function test_registeringTooManyOraclesFails() public {
uint8 max_oracles = migration.MAX_ORACLES();
address[] memory manyOracles = new address[](max_oracles + 1);
for (uint256 i = 0; i < manyOracles.length; i++) {
manyOracles[i] = vm.addr(i + 1);
}
vm.expectRevert(abi.encodeWithSelector(BridgeBase.MaxOraclesExceeded.selector));
new GrantsMigration(manyOracles, nodl, grants, uint8(manyOracles.length / 2 + 1), delay);
}
}

0 comments on commit d1c1049

Please sign in to comment.