Skip to content

Instantly share code, notes, and snippets.

@antsankov
Last active September 5, 2020 05:08
Show Gist options
  • Save antsankov/f6c614bb1df63e10bb885bbb8b022255 to your computer and use it in GitHub Desktop.
Save antsankov/f6c614bb1df63e10bb885bbb8b022255 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.7.0;
/***
*
* Structue of Treasury
* - Every block 20% of all block reward goes to this contract with deposit(false), if miner has no faith in the system they can call deposit(true) and funds are sent to burn address.
* - There are 3 triads addresses (ETCLabs, ETCCoop, IOHK) - any policy requires consensus of 2 of 3.
* - A proposal is made with a claiment address, amount of ether to be claimed, blockNumber that activates the claim.
* - If a poroposal gets support of 2 of 3 triads, proposal funds can be collected after the blockNumber, with redeem() which pays out to the claiment.
* - If 2 triads lose confidence in the third, they can vote to replace them. Requires 2/3 votes.
*/
contract Treasury {
/*=================================
= MODIFIERS =
=================================*/
modifier onlyTriad() {
require(msg.sender == ALPHA || msg.sender == BETA || msg.sender == GAMMA);
_;
}
/*=====================================
= STATIC VARS =
=====================================*/
address payable ALPHA = 0x1111111111111111111111111111111111111111;
address payable BETA = 0x2222222222222222222222222222222222222222;
address payable GAMMA = 0x3333333333333333333333333333333333333333;
address payable BURN_ADDRESS = 0x0000000000000000000000000000000000000000;
/*================================
= DATASTRUCT =
================================*/
struct Proposal {
address claimer;
uint256 amount;
uint256 blockNumber;
}
// map of propsal hash to votes on that proposal
mapping(bytes32 => bool[3]) proposalLibrary_;
mapping(address => bool[3]) confidence_;
/*=======================================
= PUBLIC FUNCTIONS =
=======================================*/
// The treasury is launched with all Triads confident in each other.
constructor()
{
confidence_[ALPHA] = [true,true,true];
confidence_[BETA] = [true,true,true];
confidence_[GAMMA] = [true,true,true];
}
/**
* Recieves Ether from Block and then gives option to burn, returns UINT amount it recieved.
*/
function deposit(bool burn)
public
payable
returns(uint256)
{
// transfer to burn address if selected, otherwise hold funds.
if (burn){
BURN_ADDRESS.transfer(msg.value);
}
return(msg.value);
}
/**
* Takes in a prposal of who gets paid, the amount of ether, and the blockNumber they can do it after. Anyone can propose.
*/
function propose(address _claimer, uint256 _amount, uint256 _blockNumber)
public
{
// Proposal memory givenProposal = Proposal({ claimer: _claimer, amount: _amount, blockNumber: _blockNumber });
bytes32 proposalHash = keccak256(abi.encodePacked(_claimer, _amount, _blockNumber));
// Require that nothing is set at that hash. If there is any truth value there, this function will throw.
require ((proposalLibrary_[proposalHash][0] || proposalLibrary_[proposalHash][1] || proposalLibrary_[proposalHash][2]) == false);
// Set the new propposal up.
proposalLibrary_[proposalHash] = [false,false,false];
}
/**
* Allows the Proposal to pay out, and anybody can call this function and it will payout to the desginated claimer
*/
function ClaimerRedeem(address payable _claimer, uint256 _amount, uint256 _blockNumber)
public
{
// Only can claim after the proposal block number
require(block.number > _blockNumber);
require(address(this).balance > _amount);
// Proposal memory claimerProposal = Proposal({ claimer: _claimer, amount: _amount, blockNumber: _blockNumber });
bytes32 claimerProposalHash = keccak256(abi.encodePacked(_claimer, _amount, _blockNumber));
bool[3] storage proposalSupport = proposalLibrary_[claimerProposalHash];
bool alphaVote = proposalSupport[0];
bool betaVote = proposalSupport[1];
bool gammaVote = proposalSupport[2];
if (atLeastTwoTrueVotes(alphaVote, betaVote, gammaVote)){
_claimer.transfer(_amount);
}
}
/**
* Queries the Confidence in the triads, and if one is removed, replace with the caller of this function, the Hero.
*/
function replaceTriad(address oldTriad)
public
returns(bool)
{
require(oldTriad == ALPHA || oldTriad == BETA || oldTriad == GAMMA);
// The person replacing the triad, has to call this function. This forces them to control the Private key of the new triad.
address payable HERO = msg.sender;
bool[3] storage supportForNew = confidence_[HERO];
bool[3] storage supportForOld = confidence_[oldTriad];
// Check the quorum for supporting the oldTraid
bool changeQuorum = atLeastTwoTrueVotes(supportForNew[0],supportForNew[1],supportForNew[2]);
bool remainQuorum = atLeastTwoTrueVotes(supportForOld[0],supportForOld[1],supportForOld[2]);
// If we have agreed to replace- remove the oldTriad and relace with msg.sender
require(changeQuorum && !remainQuorum);
if (oldTriad == ALPHA){
ALPHA = HERO;
}
else if (oldTriad == BETA){
BETA = HERO;
}
else if (oldTriad == GAMMA){
GAMMA = HERO;
}
// Reset the confidences, it is possible for oldTriad to come back.
confidence_[HERO] = [true, true, true];
confidence_[oldTriad] = [false, false, false];
}
/*=======================================
= TRIAD FUNCTIONS =
=======================================*/
/**
* Takes in the keccak256 hash of the proposal struct, and the bool of true= yay or nay.
*/
function voteOnProposal(bytes32 proposalHash, bool vote)
onlyTriad()
public
{
if (msg.sender == ALPHA){
proposalLibrary_[proposalHash][0] = vote;
}
else if (msg.sender == BETA){
proposalLibrary_[proposalHash][1] = vote;
}
else if (msg.sender == GAMMA){
proposalLibrary_[proposalHash][2] = vote;
}
}
/**
* A triad votes to remove another and replace it with a newTriad address.
*/
function callForChange(address newTriad, address oldTriad)
onlyTriad()
public
{
if (msg.sender == ALPHA){
confidence_[oldTriad][0] = false;
confidence_[newTriad][0] = true;
}
else if (msg.sender == BETA){
confidence_[oldTriad][1] = false;
confidence_[newTriad][1] = true;
}
else if (msg.sender == GAMMA){
confidence_[oldTriad][2] = false;
confidence_[newTriad][2] = true;
}
}
/*==========================================
= INTERNAL FUNCTIONS =
==========================================*/
// Returns true if at least two are true.
// See: https://stackoverflow.com/questions/3076078/check-if-at-least-two-out-of-three-booleans-are-true
function atLeastTwoTrueVotes(bool a, bool b, bool c)
internal
pure
returns(bool)
{
return a && (b || c) || (b && c);
}
}
@sriharish
Copy link

sriharish commented Aug 19, 2020

Hi! Here's the contract with compilation errors fixed 👍 :) cheers! I had to delete some comments to get the code to format correctly, however the top bit won't format all the way for some reason.

@sriharish
Copy link

Replace if (!confidence_[oldTriad][i]) with if (!confidence_[oldTriad][x]) in the replaceTriad function. Or just change all references to loop variable x to i since it's local.

@sriharish
Copy link

Variables supportQuorum and removalQuorum can be byte since they're only counting up to 2 if we're being picky. I believe there are some other vars that can be optimized for memory efficiency as well. Unless this is just proof of concept and you want to include more than just a triad of orgs in some revision.

@antsankov
Copy link
Author

This is awesome @srirharish - I posted a new revision, with improved clarity and reduced the size of the replaceTriad() function. Let me know what you think, since I think it addresses your issue:

https://gist.github.com/antsankov/f6c614bb1df63e10bb885bbb8b022255/revisions

Ready if you have more feedback on this revision.

RE your second point: All vars in the Ethereum Virtual machine are stored as Bytes32, even if it's a bool. So really by just going with uint256, on normal systems that would be an optimization, but here it doesn't matter during run time.

Cheers.

@sriharish
Copy link

sriharish commented Aug 26, 2020

@antsankov, here are all my edits after your last revision. I compiled and tested on the Remix online IDE using compiler version 0.4.20 at first, however made further edits for the latest solidity versions since ETC should now support them.

To compile in Remix IDE (with compiler v. 0.4.20+commit.3155dd80)

  • Add missing semi-colons.
  • Add proper initialization of "givenProposal" on line 79.
  • Add correct mapping syntax with square brackets for mapping "proposalLibrary_" on line 83.
  • Remove "storage" storage type since claimerProposal is used as a "memory" type.
  • Add parameter prefix "_" to all variables in propose, ClaimerRedeem.
  • Modify transfer function call syntax Line 109.
  • Add proper initialization of "claimerProposal" on line 99.
  • Modify "proposalSupport" to proper type bool[3] on line 102.
  • Modify "supportForNew" to proper type bool[3] on line 124.
  • Modify "supportForOld" to proper type bool[3] on line 125.
  • Modify "atLeastTwoTrueVotes" to pure since no storage variables are read or modified inside function.

To compile in more recent versions of solidity (>=0.5.0, <=0.7.0)

  • Modify function "Treasury" to constructor on line 49.
  • Modify getting contract balance with address(this) on line 97.
  • Modify keccak256 encoding in favor of keccak256(abi.encodePacked(*)) since keccak256 call cannot take multiple arguments anymore on lines 80 and 100. These changes were introduced with Solidity v. 0.5.0.
  • ̶M̶o̶d̶i̶f̶y̶ ̶p̶r̶o̶p̶s̶a̶l̶L̶i̶b̶r̶a̶r̶y̶_̶ ̶k̶e̶y̶ ̶t̶y̶p̶e̶ ̶t̶o̶ ̶b̶y̶t̶e̶s̶ ̶o̶n̶ ̶l̶i̶n̶e̶ ̶4̶1̶ ̶s̶i̶n̶c̶e̶ ̶w̶e̶ ̶a̶r̶e̶ ̶e̶n̶c̶o̶d̶i̶n̶g̶ ̶t̶y̶p̶e̶s̶ ̶o̶f̶ ̶v̶a̶r̶i̶o̶u̶s̶ ̶s̶i̶z̶e̶ ̶w̶i̶t̶h̶ ̶a̶b̶i̶.̶e̶n̶c̶o̶d̶e̶P̶a̶c̶k̶e̶d̶.̶
  • Modify all address references that include/set ALPHA, BETA and GAMMA to "payable" for transfer functions to work.
  • Modify proposalHash to explicit storage type "memory" (since it is of type bytes and compiler requires this specification) on line 154.

Disclaimers/Suggestions

  • You will get a compiler error in more recent versions of solidity for encoding structs with the keccak256 function.
  • Modify if statement to evaluate quorum to require on line 127.
  • Modify if to else if whenever evaluating if a single address is equal to one of 3 addresses.
  • Require functions can return error messages. Right now, they just throw errors.

Here's the gist. Feel free to revise yours with any part of the latest code. Let me know if there's a specific solidity version you're targeting and I can modify accordingly.

What do you think about adding an "operational" modifier that also requires a muilti-sig to disable the deposit function [edit] given a 2/3 vote by the triad?

EDIT: Disregard striked changes. Keccak256 hash of encoded arguments implemented in update. I've updated my gist.

RE about EVM: Oh! I didn't realize that. I should study this more. Thanks for letting me know.

@antsankov
Copy link
Author

Hey @sriharish , I've updated the contract, based on your revisions. Now I am ready to start testing it on a testnet. I'll keep you posted:

  • With the operational modifier, were you think the traids could choose to shut down the contract and make everything go to burn while it is active?
  • Good work on the Keccak256 packing. I think the 7.0 Solidity version is worth the upgrade in terms of reability. Fantastic work. Do you know how to test it yourself? I can show you how to do it with remix.

Cheers,

Alex

@sriharish
Copy link

Hey @antsankov, I've written some tests in remix (with some caveats). Here's the gist.

  • I've discovered a flaw in the "proposal" function where we don't check that the sender is the proposer. This could've lead to anyone being able to override (reset) the proposal if they knew the proposer address, amount requested, and block number. Here's the latest updated Treasury contract with modifier "verifySender".
  • I wasn't sure how to test any functions that needed to be called by the triad since I do not think remix has a function to get a payable test account. I'd be happy to get your advice on how to test this if you know a way.
  • I had to recompile all contracts with Solidity v.0.6.12 in remix in order to get Treasury to work with their testing library.
  • You probably already know this, but compilation of "Treausry_test.sol" will not work since the remix compiler won't be able to find "remix_accounts.sol". To compile, you'll just have to run the test.

Actually, I change my mind on the "operational" modifier. I was only talking about applying it to stop the deposit call (send deposits back to miners), but I suppose the philosophy behind burning the reward conflicts with this.

You rock, thanks Alex!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment