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

[DRAFT] Ragequit POC #614

Draft
wants to merge 1 commit into
base: verbs-objection-period-spike
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: BSD-3-Clause

/// @title The Nouns DAO executor and treasury

/*********************************
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░██░░░████░░██░░░████░░░ *
* ░░██████░░░████████░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
*********************************/

// LICENSE
// NounsDAOExecutor.sol is a modified version of Compound Lab's Timelock.sol:
// https://github.com/compound-finance/compound-protocol/blob/20abad28055a2f91df48a90f8bb6009279a4cb35/contracts/Timelock.sol
//
// Timelock.sol source code Copyright 2020 Compound Labs, Inc. licensed under the BSD-3-Clause license.
// With modifications by Nounders DAO.
//
// Additional conditions of BSD-3-Clause can be found here: https://opensource.org/licenses/BSD-3-Clause
//
// MODIFICATIONS
// NounsDAOExecutor.sol modifies Timelock to use Solidity 0.8.x receive(), fallback(), and built-in over/underflow protection
// This contract acts as executor of Nouns DAO governance and its treasury, so it has been modified to accept ETH.

pragma solidity ^0.8.6;

import { NounsDAOExecutor } from './NounsDAOExecutor.sol';

contract NounsDAOExecutorV2 is NounsDAOExecutor {

error RedeemFailed();

constructor(address admin_, uint256 delay_) NounsDAOExecutor(admin_, delay_) {}

function redeem(address account, uint256 amount) external {
require(msg.sender == admin, 'Only admin');

(bool success, ) = account.call{ value: amount, gas: 30_000 }(new bytes(0));
Copy link
Collaborator Author

@eladmallel eladmallel Nov 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you probably thought about this, just capturing for later: we probably want to do a "safe send" that uses WETH if sending ETH fails

wdyt @davidbrai ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either that, or we could use an escrow contract that the ragequitter can "pull" ETH from


// TODO do we want this to revert?
if (!success) {
revert RedeemFailed();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ interface INounsDAOExecutor {

function acceptAdmin() external;

function setPendingAdmin(address pendingAdmin_) external;

function queuedTransactions(bytes32 hash) external view returns (bool);

function queueTransaction(
Expand All @@ -441,11 +443,21 @@ interface INounsDAOExecutor {
string calldata signature,
bytes calldata data,
uint256 eta
) external payable returns (bytes memory);
) external returns (bytes memory); // TODO why was this payable but not in NounsDAOExecutor?
}

interface INounsDAOExecutorV2 is INounsDAOExecutor {
function redeem(address account, uint256 amount) external;
}

interface NounsTokenLike {
function getPriorVotes(address account, uint256 blockNumber) external view returns (uint96);

function totalSupply() external view returns (uint256);

function ownerOf(uint256 tokenId) external view returns (address);

function balanceOf(address owner) external view returns (uint256);

function transferFrom(address from, address to, uint256 tokenId) external;
}
64 changes: 61 additions & 3 deletions packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@

pragma solidity ^0.8.6;

import './NounsDAOInterfaces.sol';
import { NounsDAOEventsV2, INounsDAOExecutorV2, NounsTokenLike } from './NounsDAOInterfaces.sol';
import { NounsDAOStorageV3, DynamicQuorumParams, Receipt, ProposalState, ProposalCondensed,
Proposal, DynamicQuorumParamsCheckpoint } from './NounsDAOStorageV3.sol';
import { IERC20 } from '../interfaces/IERC20.sol';
import 'forge-std/console.sol';

contract NounsDAOLogicV3 is NounsDAOStorageV2, NounsDAOEventsV2 {
contract NounsDAOLogicV3 is NounsDAOStorageV3, NounsDAOEventsV2 {
/// @notice The name of this contract
string public constant name = 'Nouns DAO';

Expand Down Expand Up @@ -121,6 +125,7 @@ contract NounsDAOLogicV3 is NounsDAOStorageV2, NounsDAOEventsV2 {
error VetoerBurned();
error CantVetoExecutedProposal();
error CantCancelExecutedProposal();
error NounOwnerOnly();

/**
* @notice Used to initialize the contract during delegator contructor
Expand Down Expand Up @@ -164,7 +169,7 @@ contract NounsDAOLogicV3 is NounsDAOStorageV2, NounsDAOEventsV2 {
emit VotingDelaySet(votingDelay, votingDelay_);
emit ProposalThresholdBPSSet(proposalThresholdBPS, proposalThresholdBPS_);

timelock = INounsDAOExecutor(timelock_);
timelock = INounsDAOExecutorV2(timelock_);
nouns = NounsTokenLike(nouns_);
vetoer = vetoer_;
votingPeriod = votingPeriod_;
Expand Down Expand Up @@ -419,6 +424,59 @@ contract NounsDAOLogicV3 is NounsDAOStorageV2, NounsDAOEventsV2 {
emit ProposalVetoed(proposalId);
}

function ragequit(uint256[] calldata nounIds) external {
uint256 supply = nounsCirculatingSupply();

// transfer all nouns to timelock
// will revert if not owner or has approval for all nouns
// TODO: should this be allowed only for owner of the nouns?
for (uint256 i = 0; i < nounIds.length; ++i) {
nouns.transferFrom(msg.sender, address(timelock), nounIds[i]);
}

////// ETH redeem

// calculate ETH pro rata share
uint256 proRataShare = (nounIds.length * address(timelock).balance) / supply;

// apply penalty
uint256 amountOfEthToRedeem = ((10_000 - ragequitPenaltyBPs) * proRataShare) / 10_000;

// send ETH to msg.sender
timelock.redeem(msg.sender, amountOfEthToRedeem);

////// ERC20s redeem
for (uint256 i = 0; i < redeemableAssets.length; ++i) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we discussed, we want to give users the ability to specify the list of ERC20s they want to redeem

address asset = redeemableAssets[i];

proRataShare = (nounIds.length * IERC20(asset).balanceOf(address(timelock))) / supply;
uint256 amountOfTokensToRedeem = ((10_000 - ragequitPenaltyBPs) * proRataShare) / 10_000;
IERC20(asset).transferFrom(address(timelock), msg.sender, amountOfTokensToRedeem);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want to move this call into a new timelock function to remove the need for token approvals (in line with the other comment, letting minorities quit with ERC20 shares without needing majority permission)

}
}

function _setRagequitPenaltyBPs(uint32 ragequitPenaltyBPs_) external {
if (msg.sender != admin) {
revert AdminOnly();
}

// TODO: check max bps value?

ragequitPenaltyBPs = ragequitPenaltyBPs_;
}

function _addRedeemableAsset(address erc20) external {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we discussed, we want to explore the approach of letting all treasury-owned ERC20s be redeemable by default
to make ragequit fair by default without having to get DAO permission and attention to whitelist tokens via proposals

if (msg.sender != admin) {
revert AdminOnly();
}

redeemableAssets.push(erc20);
}

function nounsCirculatingSupply() public view returns (uint256) {
return nouns.totalSupply() - nouns.balanceOf(address(timelock));
}

/**
* @notice Gets actions of a proposal
* @param proposalId the id of the proposal
Expand Down
170 changes: 170 additions & 0 deletions packages/nouns-contracts/contracts/governance/NounsDAOStorageV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: BSD-3-Clause

/// @title Nouns DAO Logic V3 storage

pragma solidity ^0.8.6;

import { NounsDAOProxyStorage, INounsDAOExecutorV2, NounsTokenLike } from './NounsDAOInterfaces.sol';

contract NounsDAOStorageV3 is NounsDAOProxyStorage {
/// @notice Vetoer who has the ability to veto any proposal
address public vetoer;

/// @notice The delay before voting on a proposal may take place, once proposed, in blocks
uint256 public votingDelay;

/// @notice The duration of voting on a proposal, in blocks
uint256 public votingPeriod;

/// @notice The basis point number of votes required in order for a voter to become a proposer. *DIFFERS from GovernerBravo
uint256 public proposalThresholdBPS;

/// @notice The basis point number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed. *DIFFERS from GovernerBravo
uint256 public quorumVotesBPS;

/// @notice The total number of proposals
uint256 public proposalCount;

/// @notice The address of the Nouns DAO Executor NounsDAOExecutor
INounsDAOExecutorV2 public timelock;

/// @notice The address of the Nouns tokens
NounsTokenLike public nouns;

/// @notice The official record of all proposals ever proposed
mapping(uint256 => Proposal) internal _proposals;

/// @notice The latest proposal for each proposer
mapping(address => uint256) public latestProposalIds;

DynamicQuorumParamsCheckpoint[] public quorumParamsCheckpoints;

/// @notice Pending new vetoer
address public pendingVetoer;

uint256 public lastMinuteWindowInBlocks;
uint256 public objectionPeriodDurationInBlocks;

uint32 public ragequitPenaltyBPs;

address[] public redeemableAssets;
}

/// @notice Ballot receipt record for a voter
struct Receipt {
/// @notice Whether or not a vote has been cast
bool hasVoted;
/// @notice Whether or not the voter supports the proposal or abstains
uint8 support;
/// @notice The number of votes the voter had, which were cast
uint96 votes;
}

struct Proposal {
/// @notice Unique id for looking up a proposal
uint256 id;
/// @notice Creator of the proposal
address proposer;
/// @notice The number of votes needed to create a proposal at the time of proposal creation. *DIFFERS from GovernerBravo
uint256 proposalThreshold;
/// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed at the time of proposal creation. *DIFFERS from GovernerBravo
uint256 quorumVotes;
/// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds
uint256 eta;
/// @notice the ordered list of target addresses for calls to be made
address[] targets;
/// @notice The ordered list of values (i.e. msg.value) to be passed to the calls to be made
uint256[] values;
/// @notice The ordered list of function signatures to be called
string[] signatures;
/// @notice The ordered list of calldata to be passed to each call
bytes[] calldatas;
/// @notice The block at which voting begins: holders must delegate their votes prior to this block
uint256 startBlock;
/// @notice The block at which voting ends: votes must be cast prior to this block
uint256 endBlock;
/// @notice Current number of votes in favor of this proposal
uint256 forVotes;
/// @notice Current number of votes in opposition to this proposal
uint256 againstVotes;
/// @notice Current number of votes for abstaining for this proposal
uint256 abstainVotes;
/// @notice Flag marking whether the proposal has been canceled
bool canceled;
/// @notice Flag marking whether the proposal has been vetoed
bool vetoed;
/// @notice Flag marking whether the proposal has been executed
bool executed;
/// @notice Receipts of ballots for the entire set of voters
mapping(address => Receipt) receipts;
/// @notice The total supply at the time of proposal creation
uint256 totalSupply;
/// @notice The block at which this proposal was created
uint256 creationBlock;
uint256 objectionPeriodEndBlock;
}

/// @notice Possible states that a proposal may be in
enum ProposalState {
Pending,
Active,
Canceled,
Defeated,
Succeeded,
Queued,
Expired,
Executed,
Vetoed,
ObjectionPeriod
}

struct DynamicQuorumParams {
/// @notice The minimum basis point number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed.
uint16 minQuorumVotesBPS;
/// @notice The maximum basis point number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed.
uint16 maxQuorumVotesBPS;
/// @notice The dynamic quorum coefficient
/// @dev Assumed to be fixed point integer with 6 decimals, i.e 0.2 is represented as 0.2 * 1e6 = 200000
uint32 quorumCoefficient;
}

/// @notice A checkpoint for storing dynamic quorum params from a given block
struct DynamicQuorumParamsCheckpoint {
/// @notice The block at which the new values were set
uint32 fromBlock;
/// @notice The parameter values of this checkpoint
DynamicQuorumParams params;
}

struct ProposalCondensed {
/// @notice Unique id for looking up a proposal
uint256 id;
/// @notice Creator of the proposal
address proposer;
/// @notice The number of votes needed to create a proposal at the time of proposal creation. *DIFFERS from GovernerBravo
uint256 proposalThreshold;
/// @notice The minimum number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed at the time of proposal creation. *DIFFERS from GovernerBravo
uint256 quorumVotes;
/// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds
uint256 eta;
/// @notice The block at which voting begins: holders must delegate their votes prior to this block
uint256 startBlock;
/// @notice The block at which voting ends: votes must be cast prior to this block
uint256 endBlock;
/// @notice Current number of votes in favor of this proposal
uint256 forVotes;
/// @notice Current number of votes in opposition to this proposal
uint256 againstVotes;
/// @notice Current number of votes for abstaining for this proposal
uint256 abstainVotes;
/// @notice Flag marking whether the proposal has been canceled
bool canceled;
/// @notice Flag marking whether the proposal has been vetoed
bool vetoed;
/// @notice Flag marking whether the proposal has been executed
bool executed;
/// @notice The total supply at the time of proposal creation
uint256 totalSupply;
/// @notice The block at which this proposal was created
uint256 creationBlock;
}
24 changes: 24 additions & 0 deletions packages/nouns-contracts/contracts/interfaces/IERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0

/// @title Interface for ERC20

/*********************************
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░██░░░████░░██░░░████░░░ *
* ░░██████░░░████████░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
*********************************/

pragma solidity ^0.8.6;

interface IERC20 {
function balanceOf(address account) external view returns (uint256);

function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
Loading