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
Implementation of an ERC20 token with snapshots #991
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
pragma solidity ^0.4.23; | ||
|
||
import { SnapshotToken } from "../token/ERC20/SnapshotToken.sol"; | ||
|
||
|
||
contract SnapshotTokenMock is SnapshotToken { | ||
constructor() public { | ||
uint256 initial_balance = 10000; | ||
balances[msg.sender] = initial_balance; | ||
totalSupply_ = initial_balance; | ||
emit Transfer(address(0), msg.sender, initial_balance); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
pragma solidity ^0.4.23; | ||
|
||
import { StandardToken } from "./StandardToken.sol"; | ||
import { ArrayUtils } from "../../utils/ArrayUtils.sol"; | ||
|
||
|
||
/** | ||
* @title SnapshotToken | ||
* | ||
* @dev An ERC20 token which enables taking snapshots of accounts' balances. | ||
* @dev This can be useful to safely implement voting weighed by balance. | ||
*/ | ||
contract SnapshotToken is StandardToken { | ||
using ArrayUtils for uint256[]; | ||
|
||
// The 0 id represents no snapshot was taken yet. | ||
uint256 private currSnapshotId; | ||
|
||
mapping (address => uint256[]) private snapshotIds; | ||
mapping (address => uint256[]) private snapshotBalances; | ||
|
||
event Snapshot(uint256 id); | ||
|
||
function transfer(address _to, uint256 _value) public returns (bool) { | ||
_updateSnapshot(msg.sender); | ||
_updateSnapshot(_to); | ||
return super.transfer(_to, _value); | ||
} | ||
|
||
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { | ||
_updateSnapshot(_from); | ||
_updateSnapshot(_to); | ||
return super.transferFrom(_from, _to, _value); | ||
} | ||
|
||
function snapshot() public returns (uint256) { | ||
currSnapshotId += 1; | ||
emit Snapshot(currSnapshotId); | ||
return currSnapshotId; | ||
} | ||
|
||
function balanceOfAt(address _account, uint256 _snapshotId) public view returns (uint256) { | ||
require(_snapshotId > 0 && _snapshotId <= currSnapshotId); | ||
|
||
uint256 idx = snapshotIds[_account].findUpperBound(_snapshotId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is doing a binary search. We should calculate if gas becomes too much for long arrays. I doubt it. |
||
|
||
if (idx == snapshotIds[_account].length) { | ||
return balanceOf(_account); | ||
} else { | ||
return snapshotBalances[_account][idx]; | ||
} | ||
} | ||
|
||
function _updateSnapshot(address _account) internal { | ||
if (_lastSnapshotId(_account) < currSnapshotId) { | ||
snapshotIds[_account].push(currSnapshotId); | ||
snapshotBalances[_account].push(balanceOf(_account)); | ||
} | ||
} | ||
|
||
function _lastSnapshotId(address _account) internal returns (uint256) { | ||
uint256[] storage snapshots = snapshotIds[_account]; | ||
|
||
if (snapshots.length == 0) { | ||
return 0; | ||
} else { | ||
return snapshots[snapshots.length - 1]; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
pragma solidity ^0.4.23; | ||
|
||
import { Math } from "../math/Math.sol"; | ||
|
||
|
||
library ArrayUtils { | ||
function findUpperBound(uint256[] storage _array, uint256 _element) internal view returns (uint256) { | ||
uint256 low = 0; | ||
uint256 high = _array.length; | ||
|
||
while (low < high) { | ||
uint256 mid = Math.average(low, high); | ||
|
||
if (_array[mid] > _element) { | ||
high = mid; | ||
} else { | ||
low = mid + 1; | ||
} | ||
} | ||
|
||
// At this point at `low` is the exclusive upper bound. We will return the inclusive upper bound. | ||
|
||
if (low > 0 && _array[low - 1] == _element) { | ||
return low - 1; | ||
} else { | ||
return low; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
const SnapshotToken = artifacts.require('SnapshotTokenMock'); | ||
|
||
contract('SnapshotToken', function ([_, account1, account2]) { | ||
beforeEach(async function () { | ||
this.token = await SnapshotToken.new({ from: account1 }); | ||
}); | ||
|
||
it('can snapshot!', async function () { | ||
assert.equal(await this.token.balanceOf(account1), 10000); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we update this to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. although that may be annoying if |
||
assert.equal(await this.token.balanceOf(account2), 0); | ||
|
||
const { logs: logs1 } = await this.token.snapshot(); | ||
const snapshotId1 = logs1[0].args.id; | ||
|
||
assert.equal(await this.token.balanceOf(account1), 10000); | ||
assert.equal(await this.token.balanceOf(account2), 0); | ||
|
||
assert.equal(await this.token.balanceOfAt(account1, snapshotId1), 10000); | ||
assert.equal(await this.token.balanceOfAt(account2, snapshotId1), 0); | ||
|
||
await this.token.transfer(account2, 500, { from: account1 }); | ||
|
||
assert.equal(await this.token.balanceOf(account1), 9500); | ||
assert.equal(await this.token.balanceOf(account2), 500); | ||
|
||
assert.equal(await this.token.balanceOfAt(account1, snapshotId1), 10000); | ||
assert.equal(await this.token.balanceOfAt(account2, snapshotId1), 0); | ||
|
||
const { logs: logs2 } = await this.token.snapshot(); | ||
const snapshotId2 = logs2[0].args.id; | ||
|
||
assert.equal(await this.token.balanceOf(account1), 9500); | ||
assert.equal(await this.token.balanceOf(account2), 500); | ||
|
||
assert.equal(await this.token.balanceOfAt(account1, snapshotId2), 9500); | ||
assert.equal(await this.token.balanceOfAt(account2, snapshotId2), 500); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could use
AutoIncrementing
here if it happens to be available when this is merged