Skip to content
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

Quadratic TCR Feature #10

Open
wants to merge 4 commits into
base: feat/quadraticTCR
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 294 additions & 0 deletions contracts/QuadraticTCR/QuadTCR.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
pragma solidity ^0.8.7;
//SPDX-License-Identifier: MIT
import "hardhat/console.sol";

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";

interface IBAAL {
function sharesToken() external returns (address);

function lootToken() external returns (address);
}

interface IBAALTOKEN {
function getCurrentSnapshotId() external returns (uint256);

function balanceOfAt(address account, uint256 snapshotId)
external
returns (uint256);
}

//*********************************************************************//
// --------------------------- custom errors ------------------------- //
//*********************************************************************//
error INVALID_AMOUNT();
error NOT_OWNER();
error TOKENS_ALREADY_RELAEASED();
error TOKENS_ALREADY_CLAIMED();

/**
@title DAO Signal Quadratic Contract
@notice Signal with a snapshot of current loot and shares on a MolochV3 DAO
naive TCR implementation
A dao should deploy and initialize this after taking a snapshot on shares/loot
choice ids can map to a offchain db or onchain dhdb

TODO: PLCR secret voting, add actions and zodiac module for execution
*/
contract DhQuadTCR is Initializable {
event VoteCasted(
uint56 voteId,
address indexed voter,
uint152 amount,
uint48 choiceId
);

event TokensReleased(
uint56 voteId,
address indexed voter,
uint152 amount,
uint48 choiceId
);

event ClaimTokens(
address indexed voter,
uint256 amount
);

event Init(
uint256 sharesSnapshotId,
uint256 lootSnapshotId
);

/// @notice dao staking token contract instance.
IBAAL public baal;
IBAALTOKEN public baalShares;
IBAALTOKEN public baalLoot;
uint256 public sharesSnapshotId;
uint256 public lootSnapshotId;

/// @notice vote struct array.
Vote[] public votes;

/// @notice Vote struct.
struct Vote {
bool released;
address voter;
uint152 amount;
uint48 choiceId;
uint56 voteId;
}

/// @notice BatchVote struct.
struct BatchVoteParam {
uint48 choiceId;
uint152 amount;
}

/// @notice UserBalance struct.
struct UserBalance {
bool claimed;
uint256 balance;
}

/// @notice mapping which tracks the votes for a particular user.
mapping(address => uint56[]) public voterToVoteIds;

/// @notice mapping which tracks the claimed balance for a particular user.
mapping(address => UserBalance) public voterBalances;

/**
@dev initializer.
@param _baalAddress dao staking baal address.
*/
function setUp(address _baalAddress) public initializer {
baal = IBAAL(_baalAddress);
baalShares = IBAALTOKEN(baal.sharesToken());
baalLoot = IBAALTOKEN(baal.lootToken());
// get current snapshot ids
sharesSnapshotId = baalShares.getCurrentSnapshotId();
lootSnapshotId = baalLoot.getCurrentSnapshotId();
// emit event with snapshot ids
emit Init(sharesSnapshotId, lootSnapshotId);
}

/**
@dev Get Current Timestamp.
@return current timestamp.
*/
function currentTimestamp() external view returns (uint256) {
return block.timestamp;
}

/**
@dev User claims balance at snapshot.
@return snapshot total balance.
*/
function claim(address account) public returns (uint256) {
if(voterBalances[account].claimed) {
revert TOKENS_ALREADY_CLAIMED();
}
voterBalances[account].claimed = true;
voterBalances[account].balance =
baalShares.balanceOfAt(account, sharesSnapshotId) +
baalLoot.balanceOfAt(account, lootSnapshotId);
emit ClaimTokens(account, voterBalances[account].balance);
return voterBalances[account].balance;
}

/**
@dev Checks if tokens are locked or not.
@return status of the tokens.
*/
function areTokensLocked(uint56 _voteId) external view returns (bool) {
return !votes[_voteId].released;
}

/**
@dev Vote Info for a user.
@param _voter address of voter
@return Vote struct for the particular user id.
*/
function getVotesForAddress(address _voter)
external
view
returns (Vote[] memory)
{
uint56[] memory voteIds = voterToVoteIds[_voter];
Vote[] memory votesForAddress = new Vote[](voteIds.length);
for (uint256 i = 0; i < voteIds.length; i++) {
votesForAddress[i] = votes[voteIds[i]];
}
return votesForAddress;
}

/**
@dev Stake and get Voting rights.
@param _choiceId choice id.
@param _amount amount of tokens to lock.
*/
function _vote(uint48 _choiceId, uint152 _amount) internal {
if (
_amount == 0 ||
voterBalances[msg.sender].balance == 0 ||
voterBalances[msg.sender].balance < _amount
) {
revert INVALID_AMOUNT();
}

uint152 quadraticAmount = _amount * _amount;
if (voterBalances[msg.sender].balance < quadraticAmount) {
revert INVALID_AMOUNT();
}

voterBalances[msg.sender].balance -= quadraticAmount;

uint56 voteId = uint56(votes.length);

votes.push(
Vote({
voteId: voteId,
voter: msg.sender,
amount: _amount,
choiceId: _choiceId,
released: false
})
);

voterToVoteIds[msg.sender].push(voteId);

// todo: index, maybe push choice id to dhdb
emit VoteCasted(voteId, msg.sender, _amount, _choiceId);
}


/**
@dev Stake and get Voting rights in batch.
@param _batch array of struct to stake into multiple choices.
*/
function vote(BatchVoteParam[] calldata _batch) external {
for (uint256 i = 0; i < _batch.length; i++) {
_vote(_batch[i].choiceId, _batch[i].amount);
}
}

/**
@dev Sender claim and stake in batch
@param _batch array of struct to stake into multiple choices.
*/
function claimAndVote(BatchVoteParam[] calldata _batch) external {
claim(msg.sender);
for (uint256 i = 0; i < _batch.length; i++) {
_vote(_batch[i].choiceId, _batch[i].amount);
}
}


/**
@dev Calculates the quadratic cost of voting based on the desired number of votes.
@param _votes number of desired votes.
@return quadratic cost.
*/
function calculateQuadraticAmount(uint152 _votes) internal pure returns (uint152) {
return uint152(_votes * _votes);
}


/**
@dev Release tokens and give up votes.
@param _voteIds array of vote ids in order to release tokens.
*/
function releaseTokens(uint256[] calldata _voteIds) external {
for (uint256 i = 0; i < _voteIds.length; i++) {
if (votes[_voteIds[i]].voter != msg.sender) {
revert NOT_OWNER();
}
if (votes[_voteIds[i]].released) {
// UI can send the same vote multiple times, ignore it
continue;
}
votes[_voteIds[i]].released = true;

voterBalances[msg.sender].balance += votes[_voteIds[i]].amount;

emit TokensReleased(
uint56(_voteIds[i]),
msg.sender,
votes[_voteIds[i]].amount,
votes[_voteIds[i]].choiceId
);
}
}
}

contract DhQuadTCRSumoner {
address public immutable _template;

event SummonDaoStake(
address indexed signal,
address indexed baal,
uint256 date,
string details
);

constructor(address template) {
_template = template;
}

function summonSignalTCR(address baal, string calldata details)
external
returns (address)
{

DhSignalTCR signal = DhSignalTCR(Clones.clone(_template));

signal.setUp(baal);

// todo: set as module on baal avatar

emit SummonDaoStake(address(signal), address(baal), block.timestamp, details);

return (address(signal));
}
}
12 changes: 12 additions & 0 deletions contracts/QuadraticTCR/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changes from original TCR contract.

The original contract was modified to support quadratic voting by calculating the quadratic cost of voting based on the desired number of votes. This was done by introducing a new internal function calculateQuadraticAmount() which returns the quadratic cost, and by updating the _vote() function to check if the user has enough balance for the quadratic cost of their desired votes. The function _vote() now also deducts the quadratic cost from the voter's balance and uses the amount squared as the actual amount voted.

The changes made to the original contract to support quadratic voting are as follows:

- The new internal function calculateQuadraticAmount(uint152 _votes) internal pure returns (uint152) was added to calculate the quadratic cost of voting based on the desired number of votes.
- The _vote(uint48 _choiceId, uint152 _amount) internal function was updated to calculate the quadratic cost of the desired number of votes using calculateQuadraticAmount() and to deduct the quadratic cost from the voter's balance. The amount squared is used as the actual amount voted.
- The vote(BatchVoteParam[] calldata _batch) external function was modified to use _vote() for each element in the batch.
- The claimAndVote(BatchVoteParam[] calldata _batch) external function was modified to call the claim(address account) public returns (uint256) function to claim the voter's balance at the snapshot before staking and getting voting rights.

These changes enable the contract to support quadratic voting by calculating the quadratic cost of voting based on the desired number of votes.
85 changes: 85 additions & 0 deletions contracts/QuadraticTCR/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# DhQuadTCR

The DhQuadTCR contract is a Signal Quadratic Contract based on the Moloch V3 DAO. It is a naive TCR implementation where a DAO deploys and initializes this contract after taking a snapshot of shares/loot. Choice IDs can map to an off-chain database or on-chain DHDB.

## Functions
### setUp(address _baalAddress)

Initializer function which sets up the contract. It takes the address of the DAO staking BAAL token contract as an argument.

### currentTimestamp() external view returns (uint256)

Returns the current timestamp.

### claim(address account) public returns (uint256)

Allows users to claim their balance at a snapshot. Returns the snapshot total balance.

### areTokensLocked(uint56 _voteId) external view returns (bool)

Checks whether the tokens are locked or not. Returns the status of the tokens.

### getVotesForAddress(address _voter) external view returns (Vote[] memory)

Returns the vote struct for a particular user id.

### _vote(uint48 _choiceId, uint152 _amount) internal

Stake and get voting rights function. Takes the choice id and amount of tokens to lock as arguments.

### vote(BatchVoteParam[] calldata _batch) external

Stake and get voting rights in batch. Takes an array of struct to stake into multiple choices as an argument.

### claimAndVote(BatchVoteParam[] calldata _batch) external

Sender claim and stake in batch. Takes an array of struct to stake into multiple choices as an argument.

### calculateQuadraticAmount(uint152 _votes) internal pure returns (uint152)

Calculates the quadratic cost of voting based on the desired number of votes.

### releaseTokens(uint256[] calldata _voteIds) external

Release tokens and give up votes function. Takes an array of vote ids in order to release tokens as an argument.

## Events

### VoteCasted(uint56 voteId, address indexed voter, uint152 amount, uint48 choiceId)

Emits when a user casts their vote.

### TokensReleased(uint56 voteId, address indexed voter, uint152 amount, uint48 choiceId)

Emits when tokens are released by the voter.

### ClaimTokens(address indexed voter, uint256 amount)

Emits when a user claims their tokens.

### Init(uint256 sharesSnapshotId, uint256 lootSnapshotId)

Emits the shares and loot snapshot ids.

## Custom Errors
### INVALID_AMOUNT()

Thrown when the amount is invalid.

### NOT_OWNER()

Thrown when the caller is not the owner.

### TOKENS_ALREADY_RELAEASED()

Thrown when the tokens are already released.

### TOKENS_ALREADY_CLAIMED()

Thrown when the tokens are already claimed.

## Dependencies

hardhat/console.sol
@openzeppelin/contracts/proxy/utils/Initializable.sol
@openzeppelin/contracts/proxy/Clones.sol