Last active
April 17, 2021 19:48
-
-
Save seniorjoinu/6b4ca431d7fa5eccb743693aec8b18f5 to your computer and use it in GitHub Desktop.
ERC777 with history
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
contract ERC777WithHistory is ERC777 { | |
struct BalanceSnapshot { | |
uint256 timestamp; | |
uint256 balance; | |
} | |
/** | |
* @dev Each account has a history of how it's balance changed over time | |
*/ | |
mapping(address => BalanceSnapshot[]) balanceSnapshots; | |
constructor( | |
string memory name_, | |
string memory symbol_, | |
address[] memory defaultOperators_ | |
) | |
public | |
ERC777(name_, symbol_, defaultOperators_) | |
{ | |
} | |
/** | |
* @dev This function returns a balance of the account at any moment of history | |
*/ | |
function balanceAt(address account, uint256 timestamp) external view returns (uint256) { | |
BalanceSnapshot[] storage accountHistory = balanceSnapshots[account]; | |
// if the timestamp is earlier than the first balance snapshot - it's balance is 0 | |
// or if there is no history for account - it's balance is 0 | |
uint256 historyLength = accountHistory.length; | |
if (historyLength == 0 || timestamp < accountHistory[0].timestamp) { | |
return 0; | |
} | |
uint256 lastIndex = historyLength.sub(1); | |
// if the timestamp is more recent than the last balance snapshot - it's balance is the last balance | |
if (timestamp >= accountHistory[lastIndex].timestamp) { | |
return accountHistory[lastIndex].balance; | |
} | |
// otherwise - binary search based lookup | |
uint256 snapshotIdx = balanceSnapshotLookup(accountHistory, 0, lastIndex, timestamp); | |
return accountHistory[snapshotIdx].balance; | |
} | |
/** | |
* @dev This function helps the user save some fee money, when they call some other function that | |
* invokes balanceOf(account, timestamp). By calling this function the user deletes their history except the most | |
* recent entry. The user should understand that after invoking that function, they are no longer able to prove their | |
* balance history. | |
*/ | |
function clearAccountHistory() external { | |
BalanceSnapshot[] storage accountHistory = balanceSnapshots[_msgSender()]; | |
uint256 historyLength = accountHistory.length; | |
// if the callers history is empty or contains only one snapshot - return | |
if (historyLength < 2) { | |
return; | |
} | |
// otherwise delete callers history except the most recent snapshot | |
BalanceSnapshot memory recentSnapshot = accountHistory[historyLength.sub(1)]; | |
delete balanceSnapshots[_msgSender()]; | |
accountHistory.push(recentSnapshot); | |
} | |
// called on every transfer by _beforeTokenTransfer() | |
function updateAccountHistory(address account, uint256 accountBalance) internal { | |
BalanceSnapshot[] storage accountHistory = balanceSnapshots[account]; | |
// if history is empty - just add new entry | |
uint256 historyLength = accountHistory.length; | |
if (historyLength == 0) { | |
accountHistory.push(BalanceSnapshot(block.timestamp, accountBalance)); | |
} else { | |
BalanceSnapshot storage lastSnapshot = accountHistory[historyLength.sub(1)]; | |
if (lastSnapshot.timestamp == block.timestamp) { | |
// if there are multiple updates during one block - only save the most recent balance per block | |
lastSnapshot.balance = accountBalance; | |
} else { | |
// otherwise just add new balance snapshot | |
accountHistory.push(BalanceSnapshot(block.timestamp, accountBalance)); | |
} | |
} | |
} | |
// Uses binary search to find the closest to timestamp balance snapshot | |
function balanceSnapshotLookup( | |
BalanceSnapshot[] storage accountHistory, | |
uint256 begin, | |
uint256 end, | |
uint256 timestamp | |
) internal view returns (uint256) { | |
// split in half | |
uint256 midLeft = begin.add((end.sub(begin)) / 2); | |
uint256 midRight = midLeft.add(1); | |
uint256 leftTimestamp = accountHistory[midLeft].timestamp; | |
uint256 rightTimestamp = accountHistory[midRight].timestamp; | |
// if we're in between (left is lower, right is higher) or if we found exact value - return its index | |
if ((leftTimestamp <= timestamp && rightTimestamp > timestamp)) { | |
return midLeft; | |
} | |
if (rightTimestamp == timestamp) { | |
return midRight; | |
} | |
// if we're higher than both left and right, repeat for the left side | |
if (leftTimestamp < timestamp && rightTimestamp < timestamp) { | |
return balanceSnapshotLookup(accountHistory, midRight, end, timestamp); | |
} | |
// if we're lower than both left and right, repeat for the right side | |
if (leftTimestamp > timestamp && rightTimestamp > timestamp) { | |
return balanceSnapshotLookup(accountHistory, begin, midLeft, timestamp); | |
} | |
// it is impossible, because we checked boundaries before | |
assert(false); | |
return Utils.MAX_UINT; | |
} | |
/** | |
* Using openzeppelins hook to update history on every change | |
*/ | |
function _beforeTokenTransfer(address operator, address from, address to, uint256 amount) internal override virtual { | |
if (from != Utils.EMPTY_ADDRESS) { | |
updateAccountHistory(from, balanceOf(from).sub(amount)); | |
} | |
if (to != Utils.EMPTY_ADDRESS) { | |
updateAccountHistory(to, balanceOf(to).add(amount)); | |
} | |
super._beforeTokenTransfer(operator, from, to, amount); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment