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

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