-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(MintSplitter): implement core of mint splitter
- Loading branch information
1 parent
e4dc5bd
commit 1ee30ec
Showing
2 changed files
with
262 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Mint Splitter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
pragma solidity >=0.8.13; | ||
|
||
import "../migration/IHub.sol"; | ||
import "../lib/Math64x64.sol"; | ||
|
||
contract MintSplitter { | ||
// Type declarations | ||
|
||
struct Distribution { | ||
int128 allocation; | ||
uint128 sequence; // pack into the first word | ||
address destinationIterator; | ||
} | ||
|
||
// Constants | ||
|
||
address public constant SENTINEL = address(1); | ||
|
||
int128 public constant ONE_64x64 = int128(2 ** 64); | ||
|
||
/** | ||
* @notice UPDATE_RELAXATION_TIME_FEE is the minimum time before | ||
* one can update their distribution again. | ||
* This effectively introduces a fee for updating | ||
* of one minute, because any mint only counts | ||
* from after the (update + relaxation) time. | ||
*/ | ||
uint256 public constant UPDATE_RELAXATION_TIME_FEE = 1 minutes; | ||
|
||
/** | ||
* @notice there is no intention for splitting your mint | ||
* into many destinations, so putting an arbitrary cap | ||
* to set expectations for usage. | ||
*/ | ||
uint8 public constant MAX_DISTRIBUTIONS = uint8(7); | ||
|
||
// State variables | ||
|
||
/** | ||
* @notice Hub v1 contract needs to be explicitly handled | ||
* for compatibility of the original code with new hubs | ||
*/ | ||
IHubV1 public immutable hubV1; | ||
|
||
/** | ||
* @notice sources stores a linked list of all | ||
* the sources that have declared minting destinations. | ||
*/ | ||
mapping(address => address) public sources; | ||
|
||
/** | ||
* @notice destinations stores a linked list of all | ||
* the mint destinations that have been recorded. | ||
*/ | ||
mapping(address => address) public destinations; | ||
|
||
/** | ||
* @notice last updated distribution stores the timestamp | ||
* when last the source called to update their distribution. | ||
* All destinations MUST only mint their allocation from | ||
* last updated timestamp to present. | ||
*/ | ||
mapping(address => uint256) public lastUpdatedDistribution; | ||
|
||
mapping(address => mapping(address => Distribution)) public distributions; | ||
|
||
mapping(address => uint128) public sourceSequence; | ||
|
||
// Modifiers | ||
|
||
modifier canUpdate(address _source) { | ||
require( | ||
lastUpdatedDistribution[_source] < block.timestamp, "Source can not update twice at the same block time." | ||
); | ||
_; | ||
} | ||
|
||
// Constructor | ||
|
||
constructor(IHubV1 _hubV1) { | ||
require(address(_hubV1) != address(0), "Hub v1 contract must be provided."); | ||
|
||
hubV1 = _hubV1; | ||
|
||
// initialize the linked lists | ||
sources[SENTINEL] = SENTINEL; | ||
destinations[SENTINEL] = SENTINEL; | ||
} | ||
|
||
// External functions | ||
|
||
function registerDistribution(address[] calldata _destinations, int128[] calldata _allocations) | ||
external | ||
canUpdate(msg.sender) | ||
{ | ||
require(_destinations.length <= MAX_DISTRIBUTIONS, "Maximum number of split destinations is 7."); | ||
require(_destinations.length > 0, "Cannot register empty distribution."); | ||
require(_destinations.length == _allocations.length, "Each destination must have an allocation"); | ||
|
||
require( | ||
addsToOneUnit(_allocations), "Provided allocations must add to one in 64.64 bit fixed point representation." | ||
); | ||
|
||
// register the source | ||
_insertSource(msg.sender); | ||
|
||
// register the destinations, also checks destinations are not zero. | ||
for (uint256 i = 0; i < _destinations.length - 1; i++) { | ||
require( | ||
uint160(_destinations[i]) < uint160(_destinations[i + 1]), | ||
"Destinations must be unique and provided in ascending order." | ||
); | ||
_insertDestination(_destinations[i]); | ||
} | ||
_insertDestination(_destinations[_destinations.length - 1]); | ||
|
||
// add an additional relaxation time fee for updating the distribution | ||
lastUpdatedDistribution[msg.sender] = block.timestamp + UPDATE_RELAXATION_TIME_FEE; | ||
|
||
// delete all distributions for the source and initialize a new one | ||
uint128 newSequence = _deleteDistributionAndInitializeNew(msg.sender); | ||
// because the construction is involved, track a sanity check independently | ||
// in the form of a sequence number | ||
assert(newSequence == sourceSequence[msg.sender] + 1); | ||
sourceSequence[msg.sender] = newSequence; | ||
|
||
// store the new distribution | ||
_storeNewDistribution(msg.sender, newSequence, _destinations, _allocations); | ||
} | ||
|
||
function getAllocation(address _source) external returns (int128 allocation_) { | ||
require(destinations[msg.sender] != address(0), "Destination has not been registered before."); | ||
require(sources[_source] != address(0), "Source has not registered a distribution."); | ||
|
||
Distribution storage distribution = distributions[_source][msg.sender]; | ||
require( | ||
distribution.destinationIterator != address(0), "No distribution has been allocated for this destination." | ||
); | ||
assert(distribution.sequence == sourceSequence[_source]); | ||
assert(distribution.allocation >= int128(0) && distribution.allocation <= ONE_64x64); | ||
|
||
return allocation_ = distribution.allocation; | ||
} | ||
|
||
// Public functions | ||
|
||
function addsToOneUnit(int128[] calldata _allocations) public pure returns (bool unitary_) { | ||
int128 sum = int128(0); | ||
|
||
for (uint256 i = 0; i < _allocations.length; i++) { | ||
require( | ||
_allocations[i] >= int128(0) && _allocations[i] <= ONE_64x64, | ||
"Any allocation must be between zero and one." | ||
); | ||
// note: with high confidence we can simply add the int128 numbers | ||
// because we already constrain them between zero and one ... | ||
// but to be extra cautious use a range check on each addition | ||
sum = Math64x64.add(sum, _allocations[i]); | ||
} | ||
require(sum <= ONE_64x64, "Sum exceeded one unit."); | ||
// return true if the sum of all allocations adds up to one. | ||
return (sum == ONE_64x64); | ||
} | ||
|
||
// Private functions | ||
|
||
function _insertSource(address _source) private { | ||
assert(_source != address(0)); | ||
assert(_source != SENTINEL); | ||
|
||
if (sources[_source] != address(0)) { | ||
// insertion is idempotent | ||
return; | ||
} | ||
|
||
// insert new source in linked list | ||
sources[_source] = sources[SENTINEL]; | ||
sources[SENTINEL] = _source; | ||
} | ||
|
||
function _insertDestination(address _destination) private { | ||
require(_destination != address(0), "Destination cannot be zero."); | ||
require(_destination != SENTINEL, "Destination cannot be address 0x1."); | ||
|
||
if (destinations[_destination] != address(0)) { | ||
// insert is idempotent | ||
return; | ||
} | ||
|
||
// insert new source in linked list | ||
destinations[_destination] = destinations[SENTINEL]; | ||
destinations[SENTINEL] = _destination; | ||
} | ||
|
||
function _storeNewDistribution( | ||
address _source, | ||
uint128 _sequence, | ||
address[] calldata _destinations, | ||
int128[] calldata _allocations | ||
) private { | ||
// assume lengths of arrays are already checked to be equal and > 0 | ||
// and values are valid | ||
|
||
Distribution storage sentinelDistribution = distributions[_source][SENTINEL]; | ||
require( | ||
sentinelDistribution.destinationIterator == SENTINEL, | ||
"Distribution must be empty and initialized before storing new." | ||
); | ||
uint128 sequence = sentinelDistribution.sequence; | ||
require(sequence == _sequence, "Sequence number provided does not match initialized, empty distribution."); | ||
address previousIterator = SENTINEL; | ||
for (uint256 i = 0; i < _destinations.length; i++) { | ||
Distribution storage distribution = distributions[_source][_destinations[i]]; | ||
distribution.allocation = _allocations[i]; | ||
distribution.sequence = sequence; | ||
distribution.destinationIterator = previousIterator; | ||
previousIterator = _destinations[i]; | ||
} | ||
sentinelDistribution.allocation = int128(0); | ||
sentinelDistribution.destinationIterator = previousIterator; | ||
} | ||
|
||
function _deleteDistributionAndInitializeNew(address _source) private returns (uint128 sequence_) { | ||
Distribution storage sentinelDistribution = distributions[_source][SENTINEL]; | ||
if (sentinelDistribution.destinationIterator == address(0)) { | ||
// distribution is uninitialized, so initialize | ||
sentinelDistribution.allocation = int128(0); | ||
sentinelDistribution.sequence = uint128(1); | ||
sentinelDistribution.destinationIterator = SENTINEL; | ||
return sequence_ = uint128(1); | ||
} else if (sentinelDistribution.destinationIterator == SENTINEL) { | ||
// distribution is initialised, and list is empty | ||
return sequence_ = sentinelDistribution.sequence; | ||
} | ||
|
||
// handle delete by iterating over all stored distributions, | ||
// deleting them one by one and resetting the sentinel. | ||
// we opt to perform O(N) operations for updating the distribution | ||
// so that we can retrieve distribution allocations in O(1) time | ||
// with the mapping. | ||
uint8 count = 0; | ||
uint128 sequence = sentinelDistribution.sequence; | ||
address nextDestination = sentinelDistribution.destinationIterator; | ||
while (nextDestination != SENTINEL) { | ||
Distribution storage distribution = distributions[_source][nextDestination]; | ||
assert(distribution.sequence == sequence); | ||
nextDestination = distribution.destinationIterator; | ||
delete distribution.allocation; | ||
delete distribution.sequence; | ||
delete distribution.destinationIterator; | ||
count++; | ||
assert(count <= MAX_DISTRIBUTIONS); | ||
} | ||
// reset the sentinel distribution | ||
sentinelDistribution.allocation = int128(0); | ||
sentinelDistribution.sequence = uint128(sequence + 1); | ||
sentinelDistribution.destinationIterator = SENTINEL; | ||
return sequence_ = sentinelDistribution.sequence; | ||
} | ||
} |