feat: forfeit logic (#38)

This commit is contained in:
Richard Ramos 2019-08-05 12:42:06 -04:00 committed by GitHub
parent af55ca0311
commit 5021d83482
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 375 additions and 68 deletions

View File

@ -1,4 +1,10 @@
const options = require("../app/js/contributors");
let secret = {};
try {
secret = require('../.secret.json');
} catch(err) {
console.dir("warning: .secret.json file not found; this is only needed to deploy to testnet or livenet etc..");
}
function getContributors () {
var addresses = options.map(a => a.value);
@ -201,10 +207,17 @@ module.exports = {
}
},
deployment: {
accounts: [{
mnemonic: "your mainnet mnemonic here",
numAddresses: "10"
}]
accounts: [
{
mnemonic: secret.mnemonic,
hdpath: secret.hdpath || "m/44'/60'/0'/0/",
numAddresses: "10"
}
],
host: `mainnet.infura.io/${secret.infuraKey}`,
port: false,
protocol: 'https',
type: "rpc"
},
"afterDeploy": [
// Add All Contributors

View File

@ -21,7 +21,7 @@ Extension:
- /kudos 500 "<person>" "<praise>"
*/
import "token/ERC20Token.sol";
import "./token/ERC20Token.sol";
contract Meritocracy {
@ -50,6 +50,7 @@ contract Meritocracy {
mapping(address => bool) public admins;
mapping(address => Contributor) public contributors;
bytes public contributorListIPFSHash;
uint public SNTforfeitedBalance;
Meritocracy public previousMeritocracy; // Reference and read from previous contract
@ -74,13 +75,13 @@ contract Meritocracy {
// Functions only Owner can call
modifier onlyOwner {
require(msg.sender == owner);
require(msg.sender == owner, "Only the owner can call this function");
_;
}
// Functions only Admin can call
modifier onlyAdmin {
require(admins[msg.sender]);
require(admins[msg.sender], "Only admins can call this function");
_;
}
@ -93,14 +94,17 @@ contract Meritocracy {
// Contributor memory cAllocator = contributors[msg.sender];
// Requirements
// require(cAllocator.addr != address(0)); // is sender a Contributor? TODO maybe relax this restriction.
uint256 individualAmount = _amount / registry.length;
uint256 individualAmount = (SNTforfeitedBalance + _amount) / registry.length;
// removing decimals
individualAmount = (individualAmount / 1 ether * 1 ether);
uint amount = individualAmount * registry.length;
uint amount = (individualAmount * registry.length) - SNTforfeitedBalance;
SNTforfeitedBalance = 0;
require(token.transferFrom(msg.sender, address(this), amount), "Couldn't transfer SNT");
require(token.transferFrom(msg.sender, address(this), amount));
// Body
// cAllocator.inPot = true;
for (uint256 i = 0; i < registry.length; i++) {
@ -119,9 +123,9 @@ contract Meritocracy {
// Locals
Contributor storage cReceiver = contributors[msg.sender];
// Requirements
require(cReceiver.addr == msg.sender); //is sender a Contributor?
require(cReceiver.received > 0); // Contributor has received some tokens
require(cReceiver.allocation == 0); // Contributor must allocate all Token (or have Token burnt) before they can withdraw.
require(cReceiver.addr == msg.sender, "Not a contributor"); //is sender a Contributor?
require(cReceiver.received > 0, "No tokens to withdraw"); // Contributor has received some tokens
require(cReceiver.allocation == 0, "Allocation needs to be awarded or forfeited"); // Contributor must allocate all Token (or have Token burnt) before they can withdraw.
// require(cReceiver.inPot); // Contributor has put some tokens into the pot
// Body
uint256 r = cReceiver.received;
@ -236,7 +240,13 @@ contract Meritocracy {
// Swap & Pop!
registry[idx] = registry[registryLength];
registry.pop();
delete contributors[c]; // TODO check if this works
// Automatically withdraw ex-contributor SNT and increasing the forfeited balance
Contributor storage contrib = contributors[c];
token.transfer(contrib.addr, contrib.received);
SNTforfeitedBalance += contrib.allocation;
delete contributors[c];
// Set new IPFS hash for the list
contributorListIPFSHash = _contributorListIPFSHash;
emit ContributorRemoved(c);
@ -260,6 +270,7 @@ contract Meritocracy {
lastForfeit = block.timestamp;
for (uint256 i = 0; i < registryLength; i++) { // should never be longer than maxContributors, see addContributor
Contributor storage c = contributors[registry[i]];
SNTforfeitedBalance += c.allocation;
c.totalForfeited += c.allocation; // Shaaaaame!
c.allocation = 0;
// cReceiver.inPot = false; // Contributor has to put tokens into next round
@ -335,10 +346,6 @@ contract Meritocracy {
// Constructor ------------------------------------------------------------------------------------------
// constructor(address _token, uint256 _maxContributors, address _previousMeritocracy) public {
// }
// Set Owner, Token address, initial maxContributors
constructor(address _token, uint256 _maxContributors, bytes memory _contributorListIPFSHash) public {
// Body
@ -346,7 +353,7 @@ contract Meritocracy {
addAdmin(owner);
lastForfeit = block.timestamp;
token = ERC20Token(_token);
maxContributors= _maxContributors;
maxContributors = _maxContributors;
contributorListIPFSHash = _contributorListIPFSHash;
// previousMeritocracy = Meritocracy(_previousMeritocracy);
// importPreviousMeritocracyData() TODO

View File

@ -9,7 +9,7 @@
"buildDir": "dist/",
"config": "config/",
"versions": {
"solc": "0.5.0",
"solc": "0.5.9",
"ipfs-api": "17.2.4"
},
"plugins": {

View File

@ -31,7 +31,9 @@
"@fortawesome/free-solid-svg-icons": "^5.8.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"bootstrap": "^4.3.1",
"embark-solc": "^4.0.1",
"embark": "^4.0.2",
"embark-solc": "^4.0.3",
"embark-solium": "0.1.0",
"embarkjs-connector-web3": "^4.0.0",
"moment": "^2.24.0",
"react": "^16.8.6",

View File

@ -1,7 +1,7 @@
/*global contract, config, it, assert*/
/* global contract, config, it, assert, web3, before, describe, xit */
const Meritocracy = require('Embark/contracts/Meritocracy');
// const StandardToken = require('Embark/contracts/StandardToken');
const SNT = require('Embark/contracts/SNT');
const TestUtils = require('../utils/testUtils');
let accounts;
let owner;
@ -24,7 +24,7 @@ config({
]
},
contracts: {
"MiniMeToken": {"deploy": false, "args": []},
"MiniMeToken": {"deploy": false},
"MiniMeTokenFactory": {},
"SNT": {
"instanceOf": "MiniMeToken",
@ -32,14 +32,13 @@ config({
"$MiniMeTokenFactory",
"0x0000000000000000000000000000000000000000",
0,
"TestMiniMeToken",
"Status Network Token",
18,
"STT",
true
]
},
"Meritocracy": {
"fromIndex": 0, // accounts[0]
"args": ["$SNT", 10, IPFS_HASH] // Bind to SNT Contract, max 10 contributors.
}
}
@ -47,7 +46,7 @@ config({
accounts = web3_accounts;
owner = accounts[0];
admins = [accounts[0], accounts[1], accounts[2]];
ownerInitTokens = 10000;
ownerInitTokens = web3.utils.toWei("10000", "ether");
});
contract("Meritocracy", function () {
@ -64,15 +63,14 @@ contract("Meritocracy", function () {
assert.strictEqual(result, owner);
result = await Meritocracy.methods.maxContributors().call();
assert.strictEqual(parseInt(result), 10);
assert.strictEqual(parseInt(result, 10), 10);
});
it("registry.length == 3, allocate(1000);", async function () {
var result;
let allocationAmount = 1000;
let allocationAmount = web3.utils.toWei("1000", "ether"); // 1000 SNT
let contributorCount = 3;
let individualAllocation = parseInt(allocationAmount / contributorCount); // 333
// Add 3 Contributors and check registry length matches
var i = 0;
@ -80,8 +78,9 @@ contract("Meritocracy", function () {
result = await Meritocracy.methods.addContributor(accounts[i], IPFS_HASH).send({from: owner});
i++;
}
let registry = await Meritocracy.methods.getRegistry().call(); // TODO check if this works
assert.strictEqual(parseInt(registry.length), contributorCount); // 3
assert.strictEqual(parseInt(registry.length, 10), contributorCount); // 3
// Approve and allocate 1000 SNT for Meritocracy use
result = await SNT.methods.approve(Meritocracy.options.address, allocationAmount).send({from: owner});
@ -89,62 +88,201 @@ contract("Meritocracy", function () {
// FIXME these don't work. Looks like the allocation doesn't go through
result = await SNT.methods.balanceOf(Meritocracy.address).call();
// assert.strictEqual(parseInt(result), allocationAmount); // 1000
assert.strictEqual(result, web3.utils.toWei("999", "ether")); // 999, because each contributor will receive 333.
result = await SNT.methods.balanceOf(owner).call();
// assert.strictEqual(parseInt(result), ownerInitTokens - allocationAmount); // 9000
assert.strictEqual(result, web3.utils.toBN(ownerInitTokens).sub(web3.utils.toBN(web3.utils.toWei("999", "ether"))).toString()); // 9001
// Check Individual Contributor amount is 333
const contributor = await Meritocracy.methods.contributors(admins[0]).call();
// assert.strictEqual(parseInt(contributor.allocation), individualAllocation); // 333
});
assert.strictEqual(contributor.allocation, web3.utils.toWei("333", "ether"));
});
// TODO Addadmin
// TODO RemoveAdmin
// TODO award
describe('award', () => {
it('should be able to award a contributor', async () => {
await Meritocracy.methods.award(accounts[0], web3.utils.toWei("25", "ether"), "ABC").send({from: accounts[2]});
it("maxContributor + 1 fails", async function() {
// TODO change so admin adds them
var result;
let contributorCount = 3;
let additionalContributorsToMax = 7;
var i = 0;
while (i < additionalContributorsToMax) {
result = await Meritocracy.methods.addContributor(accounts[contributorCount + i], IPFS_HASH).send({from: owner});
i++;
}
try {
result = await Meritocracy.methods.addContributor(accounts[i], IPFS_HASH).send({from: owner});
assert.fail('should have reverted');
} catch (error) {
assert.strictEqual(error.message, "VM Exception while processing transaction: revert");
}
const sender = await Meritocracy.methods.contributors(accounts[2]).call();
assert.strictEqual(sender.allocation, web3.utils.toBN(web3.utils.toWei("333", "ether")).sub(web3.utils.toBN(web3.utils.toWei("25", "ether"))).toString());
await Meritocracy.methods.award(accounts[1], web3.utils.toWei("25", "ether"), "ABC").send({from: accounts[2]});
});
xit('should fail if awarded contributor does not exist', async () => {
// TODO:
});
xit('only contributors can do awards', async () => {
// TODO:
});
});
describe('removeContributor', () => {
it('removes with normal values', async () => {
let oldRegistry = await Meritocracy.methods.getRegistry().call();
describe('withdraw', () => {
it('cannot withdraw if user has allocation', async () => {
try {
await Meritocracy.methods.withdraw().send({from: accounts[0]});
assert.fail('should have reverted');
} catch (error) {
assert.strictEqual(error.message, "VM Exception while processing transaction: revert Allocation needs to be awarded or forfeited");
}
});
let result = await Meritocracy.methods.removeContributor(1, IPFS_HASH).send({from: owner});
it('can withdraw if allocation is 0', async () => {
const balanceBefore = await SNT.methods.balanceOf(accounts[0]).call();
const contributor = await Meritocracy.methods.contributors(accounts[0]).call();
let registry = await Meritocracy.methods.getRegistry().call();
await Meritocracy.methods.award(accounts[2], web3.utils.toWei("333", "ether"), "ABC").send({from: accounts[0]});
await Meritocracy.methods.withdraw().send({from: accounts[0]});
assert.strictEqual(registry.length, oldRegistry.length - 1);
})
})
const balanceAfter = await SNT.methods.balanceOf(accounts[0]).call();
// TODO award
// TODO withdraw before and after
assert(web3.utils.toBN(balanceAfter).gt(web3.utils.toBN(balanceBefore)))
assert.strictEqual(balanceAfter, web3.utils.toBN(balanceBefore).add(web3.utils.toBN(contributor.received)).toString());
});
});
// TODO forfeitAllocations
describe('forfeitAllocations', () => {
// TODO: add test cases
let amountToForfeit = 0;
let forfeitedBalance = 0;
it("should forfeit allocations", async () => {
const b1 = web3.utils.toBN((await Meritocracy.methods.contributors(accounts[0]).call()).allocation);
const b2 = web3.utils.toBN((await Meritocracy.methods.contributors(accounts[1]).call()).allocation);
const b3 = web3.utils.toBN((await Meritocracy.methods.contributors(accounts[2]).call()).allocation);
amountToForfeit = b1.add(b2.add(b3));
TestUtils.increaseTime(86400 * 8);
await Meritocracy.methods.forfeitAllocations().send({from: accounts[0]});
});
it("Forfeited balance should increase", async () => {
forfeitedBalance = await Meritocracy.methods.SNTforfeitedBalance().call();
assert(forfeitedBalance !== "0");
assert.strictEqual(forfeitedBalance, amountToForfeit.toString());
});
it("Balances should be correct after forfeiting the allocations", async () => {
const c1 = await Meritocracy.methods.contributors(accounts[0]).call();
const c2 = await Meritocracy.methods.contributors(accounts[1]).call();
const c3 = await Meritocracy.methods.contributors(accounts[2]).call();
const b1 = web3.utils.toBN(c1.allocation).add(web3.utils.toBN(c1.received));
const b2 = web3.utils.toBN(c2.allocation).add(web3.utils.toBN(c2.received));
const b3 = web3.utils.toBN(c3.allocation).add(web3.utils.toBN(c3.received));
const correctContractBalance = web3.utils.toBN(forfeitedBalance).add(b1.add(b2.add(b3)));
const contractBalance = await SNT.methods.balanceOf(Meritocracy.options.address).call();
assert.strictEqual(correctContractBalance.toString(), contractBalance);
});
});
// TODO withdraw after forfeitAllocations
// TODO setMaxContributors smaller than max
// TODO setMaxContributors again
// TODO addContributors
// TODO changeOwner
describe('allocate 2nd cycle', () => {
// TODO escape
// TODO changeToken
before(async () => {
// Reset approval
await SNT.methods.approve(Meritocracy.options.address, "0").send({from: owner});
});
it("should allocate new funds", async () => {
const allocationAmount = web3.utils.toWei("300", "ether"); // 300 SNT
await SNT.methods.approve(Meritocracy.options.address, allocationAmount).send({from: owner});
await Meritocracy.methods.allocate(allocationAmount).send({from: owner});
});
it("contract balance should be equivalent to new allocation (with prev forfeited balance), and received for each contributor", async () => {
const contractBalance = await SNT.methods.balanceOf(Meritocracy.options.address).call();
const c1 = await Meritocracy.methods.contributors(accounts[0]).call();
const c2 = await Meritocracy.methods.contributors(accounts[1]).call();
const c3 = await Meritocracy.methods.contributors(accounts[2]).call();
const b1 = web3.utils.toBN(c1.allocation).add(web3.utils.toBN(c1.received));
const b2 = web3.utils.toBN(c2.allocation).add(web3.utils.toBN(c2.received));
const b3 = web3.utils.toBN(c3.allocation).add(web3.utils.toBN(c3.received));
const forfeitedBalance = web3.utils.toBN(await Meritocracy.methods.SNTforfeitedBalance().call());
const correctContractBalance = forfeitedBalance.add(b1.add(b2.add(b3)));
assert.strictEqual(contractBalance, correctContractBalance.toString());
});
});
describe('removeContributor', () => {
let initialForfeitBalance = 0;
let balanceToForfeit = 0;
let contribBalance = 0;
let contribReceived = 0;
before(async () => {
initialForfeitBalance = web3.utils.toBN(await Meritocracy.methods.SNTforfeitedBalance().call());
const contributor = await Meritocracy.methods.contributors(accounts[1]).call();
balanceToForfeit = web3.utils.toBN(contributor.allocation);
contribReceived = web3.utils.toBN(contributor.received);
contribBalance = web3.utils.toBN(await SNT.methods.balanceOf(accounts[1]).call());
});
it('removes with normal values', async () => {
let oldRegistry = await Meritocracy.methods.getRegistry().call();
await Meritocracy.methods.removeContributor(1, IPFS_HASH).send({from: owner});
let registry = await Meritocracy.methods.getRegistry().call();
assert.strictEqual(registry.length, oldRegistry.length - 1);
});
it("contributor should have received their SNT", async () => {
assert(contribReceived !== '0');
const currBalance = await SNT.methods.balanceOf(accounts[1]).call();
assert.strictEqual(currBalance, contribBalance.add(contribReceived).toString());
});
it("contributor data should be removed", async() => {
const contributor = await Meritocracy.methods.contributors(accounts[1]).call();
assert.strictEqual(TestUtils.zeroAddress, contributor.addr);
});
it("forfeited balance should have increased using deleted contributor allocation", async () => {
const forfeitedBalance = await Meritocracy.methods.SNTforfeitedBalance().call();
assert.strictEqual(forfeitedBalance, initialForfeitBalance.add(balanceToForfeit).toString());
});
});
describe('other conditions', async () => {
it("maxContributor + 1 fails", async function() {
// TODO change so admin adds them
let contributorCount = 3;
let additionalContributorsToMax = 7;
var i = 0;
while (i < additionalContributorsToMax) {
await Meritocracy.methods.addContributor(accounts[contributorCount + i], IPFS_HASH).send({from: owner});
i++;
}
try {
await Meritocracy.methods.addContributor(accounts[i], IPFS_HASH).send({from: owner});
assert.fail('should have reverted');
} catch (error) {
assert.strictEqual(error.message, "VM Exception while processing transaction: revert");
}
});
});
// TODO setMaxContributors smaller than max
// TODO setMaxContributors again
// TODO addContributors
// TODO changeOwner
// TODO escape
// TODO changeToken
// TODO escape overload?
});

147
utils/testUtils.js Normal file
View File

@ -0,0 +1,147 @@
/*global assert, web3*/
// This has been tested with the real Ethereum network and Testrpc.
// Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9
exports.assertReverts = (contractMethodCall, maxGasAvailable) => {
return new Promise((resolve, reject) => {
try {
resolve(contractMethodCall());
} catch (error) {
reject(error);
}
})
.then(tx => {
assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed");
})
.catch(error => {
if ((String(error)).indexOf("invalid opcode") < 0 && (String(error)).indexOf("out of gas") < 0) {
// Checks if the error is from TestRpc. If it is then ignore it.
// Otherwise relay/throw the error produced by the above assertion.
// Note that no error is thrown when using a real Ethereum network AND the assertion above is true.
throw error;
}
});
};
exports.listenForEvent = event => new Promise((resolve, reject) => {
event({}, (error, response) => {
if (!error) {
resolve(response.args);
} else {
reject(error);
}
event.stopWatching();
});
});
exports.eventValues = (receipt, eventName) => {
if (receipt.events[eventName]) return receipt.events[eventName].returnValues;
};
exports.addressToBytes32 = (address) => {
const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + address.slice(2);
return "0x" + stringed.substring(stringed.length - 64, stringed.length);
};
// OpenZeppelin's expectThrow helper -
// Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js
exports.expectThrow = async promise => {
try {
await promise;
} catch (error) {
// TODO: Check jump destination to destinguish between a throw
// and an actual invalid jump.
const invalidOpcode = error.message.search('invalid opcode') >= 0;
// TODO: When we contract A calls contract B, and B throws, instead
// of an 'invalid jump', we get an 'out of gas' error. How do
// we distinguish this from an actual out of gas event? (The
// testrpc log actually show an 'invalid jump' event.)
const outOfGas = error.message.search('out of gas') >= 0;
const revert = error.message.search('revert') >= 0;
assert(
invalidOpcode || outOfGas || revert,
'Expected throw, got \'' + error + '\' instead',
);
return;
}
assert.fail('Expected throw not received');
};
exports.assertJump = (error) => {
assert(error.message.search('VM Exception while processing transaction') > -1, 'Revert should happen');
};
function callbackToResolve(resolve, reject) {
return function(error, value) {
if (error) {
reject(error);
} else {
resolve(value);
}
};
}
exports.promisify = (func) =>
(...args) => {
return new Promise((resolve, reject) => {
const callback = (err, data) => err ? reject(err) : resolve(data);
func.apply(this, [...args, callback]);
});
};
exports.zeroAddress = '0x0000000000000000000000000000000000000000';
exports.zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
exports.timeUnits = {
seconds: 1,
minutes: 60,
hours: 60 * 60,
days: 24 * 60 * 60,
weeks: 7 * 24 * 60 * 60,
years: 365 * 24 * 60 * 60
};
exports.ensureException = function(error) {
assert(isException(error), error.toString());
};
function isException(error) {
let strError = error.toString();
return strError.includes('invalid opcode') || strError.includes('invalid JUMP') || strError.includes('revert');
}
const evmMethod = (method, params = []) => {
return new Promise(function(resolve, reject) {
const sendMethod = (web3.currentProvider.sendAsync) ? web3.currentProvider.sendAsync.bind(web3.currentProvider) : web3.currentProvider.send.bind(web3.currentProvider);
sendMethod(
{
jsonrpc: '2.0',
method,
params,
id: new Date().getSeconds()
},
(error, res) => {
if (error) {
return reject(error);
}
resolve(res.result);
}
);
});
};
exports.evmSnapshot = async () => {
const result = await evmMethod("evm_snapshot");
return web3.utils.hexToNumber(result);
};
exports.evmRevert = (id) => {
const params = [id];
return evmMethod("evm_revert", params);
};
exports.increaseTime = async (amount) => {
await evmMethod("evm_increaseTime", [Number(amount)]);
await evmMethod("evm_mine");
};