Skip to content

Commit

Permalink
feat: refund proposal fee
Browse files Browse the repository at this point in the history
  • Loading branch information
deluca-mike committed Nov 25, 2023
1 parent 6d42497 commit 3c54ce3
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 12 deletions.
54 changes: 54 additions & 0 deletions abi/DualGovernor.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@
"name": "ExecutionFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "enum IGovernor.ProposalState",
"name": "state",
"type": "uint8"
}
],
"name": "FeeNotDestinedForVault",
"type": "error"
},
{
"inputs": [],
"name": "InvalidCallDatasLength",
Expand Down Expand Up @@ -131,6 +142,11 @@
"name": "NoAllowedCashTokens",
"type": "error"
},
{
"inputs": [],
"name": "NoProposalFee",
"type": "error"
},
{
"inputs": [],
"name": "NotSelf",
Expand Down Expand Up @@ -325,6 +341,31 @@
"name": "ProposalExecuted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "proposalId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "cashToken",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "proposalFee",
"type": "uint256"
}
],
"name": "ProposalFeeSentToVault",
"type": "event"
},
{
"anonymous": false,
"inputs": [
Expand Down Expand Up @@ -1447,6 +1488,19 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "proposalId_",
"type": "uint256"
}
],
"name": "sendProposalFeeToVault",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
2 changes: 1 addition & 1 deletion bytecode/DualGovernor.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bytecode/DualGovernorDeployer.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bytecode/Registrar.json

Large diffs are not rendered by default.

49 changes: 45 additions & 4 deletions src/DualGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ contract DualGovernor is IDualGovernor, ERC712 {
uint256 yesWeight;
}

struct ProposalFeeInfo {
address cashToken;
uint256 fee;
}

uint256 public constant ONE = 10_000;

// keccak256("Ballot(uint256 proposalId,uint8 support)")
Expand Down Expand Up @@ -68,6 +73,8 @@ contract DualGovernor is IDualGovernor, ERC712 {

mapping(uint256 proposalId => Proposal proposal) internal _proposals;

mapping(uint256 proposalId => ProposalFeeInfo proposalFee) internal _proposalFees;

mapping(uint256 proposalId => mapping(address voter => bool hasVoted)) internal _hasVoted;

mapping(uint256 epoch => uint256 count) internal _standardProposals;
Expand Down Expand Up @@ -257,10 +264,12 @@ contract DualGovernor is IDualGovernor, ERC712 {
if (proposalType_ == ProposalType.Standard) {
_standardProposals[voteStart_] += 1;

// NOTE: Not calling `distribute` on vault since:
// - anyone can do it, anytime
// - `DualGovernor` should not need to know how the vault works
ERC20Helper.transferFrom(_cashToken, msg.sender, _vault, _proposalFee);
address cashToken_ = _cashToken;
uint256 proposalFee_ = _proposalFee;

_proposalFees[proposalId_] = ProposalFeeInfo({ cashToken: cashToken_, fee: proposalFee_ });

ERC20Helper.transferFrom(cashToken_, msg.sender, address(this), proposalFee_);
}

_proposals[proposalId_] = Proposal({
Expand All @@ -287,6 +296,28 @@ contract DualGovernor is IDualGovernor, ERC712 {
);
}

function sendProposalFeeToVault(uint256 proposalId_) external {
ProposalState state_ = state(proposalId_);

// Must be expired or defeated to have the fee sent to the vault
if (state_ != ProposalState.Expired && state_ != ProposalState.Defeated) revert FeeNotDestinedForVault(state_);

uint256 proposalFee_ = _proposalFees[proposalId_].fee;

if (proposalFee_ == 0) revert NoProposalFee();

address cashToken_ = _proposalFees[proposalId_].cashToken;

delete _proposalFees[proposalId_];

emit ProposalFeeSentToVault(proposalId_, cashToken_, proposalFee_);

// NOTE: Not calling `distribute` on vault since:
// - anyone can do it, anytime
// - `DualGovernor` should not need to know how the vault works
ERC20Helper.transfer(cashToken_, _vault, proposalFee_);
}

/******************************************************************************************************************\
| External/Public View/Pure Functions |
\******************************************************************************************************************/
Expand Down Expand Up @@ -629,6 +660,16 @@ contract DualGovernor is IDualGovernor, ERC712 {
(bool success_, bytes memory data_) = address(this).call(callData_);

if (!success_) revert ExecutionFailed(data_);

uint256 proposalFee_ = _proposalFees[proposalId_].fee;

if (proposalFee_ == 0) return proposalId_;

address cashToken_ = _proposalFees[proposalId_].cashToken;

delete _proposalFees[proposalId_];

ERC20Helper.transfer(cashToken_, proposal_.proposer, proposalFee_);
}

function _removeFromList(bytes32 list_, address account_) internal {
Expand Down
12 changes: 12 additions & 0 deletions src/interfaces/IDualGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ interface IDualGovernor is IGovernorBySig {

event ZeroTokenThresholdRatioSet(uint16 thresholdRatio);

event ProposalFeeSentToVault(uint256 indexed proposalId, address indexed cashToken, uint256 proposalFee);

/******************************************************************************************************************\
| Errors |
\******************************************************************************************************************/
Expand All @@ -42,6 +44,8 @@ interface IDualGovernor is IGovernorBySig {

error ExecutionFailed(bytes data);

error FeeNotDestinedForVault(ProposalState state);

error InvalidCallDatasLength();

error InvalidCashToken();
Expand All @@ -62,6 +66,8 @@ interface IDualGovernor is IGovernorBySig {

error NoAllowedCashTokens();

error NoProposalFee();

error NotSelf();

error ProposalCannotBeExecuted();
Expand All @@ -78,6 +84,12 @@ interface IDualGovernor is IGovernorBySig {

error ZeroVaultAddress();

/******************************************************************************************************************\
| Interactive Functions |
\******************************************************************************************************************/

function sendProposalFeeToVault(uint256 proposalId) external;

/******************************************************************************************************************\
| View/Pure Functions |
\******************************************************************************************************************/
Expand Down
25 changes: 24 additions & 1 deletion test/DualGovernor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TestUtils } from "./utils/TestUtils.sol";

contract DualGovernorTests is TestUtils {
event CashTokenSet(address indexed cashToken_);
event ProposalFeeSentToVault(uint256 indexed proposalId, address indexed cashToken, uint256 proposalFee);
event ProposalFeeSet(uint256 proposalFee_);

uint256 internal constant _ONE = 10_000;
Expand Down Expand Up @@ -215,7 +216,6 @@ contract DualGovernorTests is TestUtils {
callDatas_[0] = abi.encodeWithSelector(_dualGovernor.setProposalFee.selector, 1);

uint256 proposalId_ = _dualGovernor.hashProposal(callDatas_[0]);
uint256 currentEpoch_ = _dualGovernor.clock();

_dualGovernor.setProposal(proposalId_, IDualGovernor.ProposalType.Standard, 1, 1, _powerTokenThresholdRatio);

Expand Down Expand Up @@ -271,4 +271,27 @@ contract DualGovernorTests is TestUtils {
vm.prank(address(_dualGovernor));
_dualGovernor.emergencyAddAndRemoveFromList("SOME_LIST", _alice, _bob);
}

function test_sendProposalFeeToVault_feeNotDestinedForVault() external {
uint256 proposalId_ = 1;
uint256 currentEpoch_ = _dualGovernor.clock();

_dualGovernor.setProposalFeeInfo(proposalId_, _cashToken1, 1000);
_dualGovernor.setProposal(proposalId_, IDualGovernor.ProposalType.Standard, currentEpoch_, currentEpoch_, 1);

vm.expectRevert(abi.encodeWithSelector(IDualGovernor.FeeNotDestinedForVault.selector, 1));
_dualGovernor.sendProposalFeeToVault(proposalId_);
}

function test_sendProposalFeeToVault() external {
uint256 proposalId_ = 1;

_dualGovernor.setProposalFeeInfo(proposalId_, _cashToken1, 1000);
_dualGovernor.setProposal(proposalId_, IDualGovernor.ProposalType.Standard, 1, 1, 1);

vm.expectEmit();
emit ProposalFeeSentToVault(proposalId_, _cashToken1, 1000);

_dualGovernor.sendProposalFeeToVault(proposalId_);
}
}
2 changes: 1 addition & 1 deletion test/EpochBasedInflationaryVoteToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ contract EpochBasedInflationaryVoteTokenTests is TestUtils {
}

function testFuzz_full(uint256 seed_) external {
vm.skip(true);
vm.skip(false);

for (uint256 index_; index_ < 1000; ++index_) {
// console2.log(" ");
Expand Down
8 changes: 5 additions & 3 deletions test/Integration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ contract IntegrationTests is TestUtils {
uint256 proposalId_ = governor_.propose(targets_, values_, callDatas_, description_);

assertEq(cashToken_.balanceOf(_accounts[0]), 0);
assertEq(cashToken_.balanceOf(governor_.vault()), proposalFee_);
assertEq(cashToken_.balanceOf(address(governor_)), proposalFee_);

_goToNextVoteEpoch();

Expand All @@ -108,7 +108,10 @@ contract IntegrationTests is TestUtils {

governor_.execute(targets_, values_, callDatas_, bytes32(0));

// assertEq(governor_.proposalFee(), newProposalFee_);
assertEq(governor_.proposalFee(), newProposalFee_);

assertEq(cashToken_.balanceOf(_accounts[0]), proposalFee_);
assertEq(cashToken_.balanceOf(address(governor_)), 0);
}

function test_emergencyUpdateConfig() external {
Expand Down Expand Up @@ -146,7 +149,6 @@ contract IntegrationTests is TestUtils {
function test_reset() external {
IRegistrar registrar_ = IRegistrar(_registrar);
IDualGovernor governor_ = IDualGovernor(registrar_.governor());
IPowerToken powerToken_ = IPowerToken(governor_.powerToken());

_jumpToEpoch(governor_.clock() + 1);

Expand Down
4 changes: 4 additions & 0 deletions test/utils/DualGovernorHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ contract DualGovernorHarness is DualGovernor {
});
}

function setProposalFeeInfo(uint256 proposalId_, address cashToken_, uint256 fee_) external {
_proposalFees[proposalId_] = ProposalFeeInfo({ cashToken: cashToken_, fee: fee_ });
}

function setStandardProposals(uint256 epoch_, uint256 count_) external {
_standardProposals[epoch_] = count_;
}
Expand Down

0 comments on commit 3c54ce3

Please sign in to comment.