-
Notifications
You must be signed in to change notification settings - Fork 437
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
base: verbs-objection-period-spike
Are you sure you want to change the base?
[DRAFT] Ragequit POC #614
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)); | ||
|
||
// TODO do we want this to revert? | ||
if (!success) { | ||
revert RedeemFailed(); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
||
|
@@ -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 | ||
|
@@ -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_; | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
|
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; | ||
} |
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); | ||
} |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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