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

feat: add new distributor #377

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
97 changes: 97 additions & 0 deletions contracts/distribution/PushDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import { Party } from "./../party/Party.sol";
import { PartyGovernance } from "./../party/PartyGovernance.sol";
import { ProposalExecutionEngine } from "./../proposals/ProposalExecutionEngine.sol";
import { IERC20 } from "../tokens/IERC20.sol";
import { LibSafeCast } from "../utils/LibSafeCast.sol";
import { LibERC20Compat } from "./../utils/LibERC20Compat.sol";
import { LibAddress } from "./../utils/LibAddress.sol";

contract PushDistributor {
event Distributed(Party party, IERC20 token, address[] members, uint256 amount);

error NotEnoughETH(uint256 expectedAmount, uint256 receivedAmount);
error UnexpectedETH(uint256 amount);
error WrongProposalId(uint256 proposalId);
error WrongMembers();
error MembersNotSorted();

using LibSafeCast for uint256;
using LibERC20Compat for IERC20;
using LibAddress for address payable;

// Token address used to indicate ETH.
IERC20 private constant ETH_ADDRESS = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);

function distribute(
Copy link
Contributor

Choose a reason for hiding this comment

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

Would like detailed natspec here. It's a pretty advanced function. Otherwise PR looks good.

IERC20 token,
address[] memory members,
uint256 amount,
uint256 proposalId
) external payable {
Party party = Party(payable(msg.sender));
if (token == ETH_ADDRESS && msg.value < amount) revert NotEnoughETH(amount, msg.value);

0xble marked this conversation as resolved.
Show resolved Hide resolved
uint40 proposedTime;
uint96 totalVotingPower;
{
(, PartyGovernance.ProposalStateValues memory proposal) = party.getProposalStateInfo(
proposalId
);

if (proposal.executedTime != block.timestamp) revert WrongProposalId(proposalId);

proposedTime = proposal.proposedTime;
totalVotingPower = proposal.totalVotingPower;
}

address prevMember;
uint96 totalIntrinsicVotingPower;
for (uint256 i = 0; i < members.length; i++) {
address member = members[i];

// Prevent duplicate members to prevent members array manipulation.
// For example, a member being replace with another duplicate member
// that has the same voting power.
if (member <= prevMember) revert MembersNotSorted();

prevMember = member;

uint96 intrinsicVotingPower = ProposalExecutionEngine(address(party))
.getIntrinsicVotingPowerAt(member, proposedTime, 0);

totalIntrinsicVotingPower += intrinsicVotingPower;

uint256 shareAmount = (amount * intrinsicVotingPower) / totalVotingPower;

if (shareAmount > 0) {
// Transfer the share of the distribution to the member.
_transfer(token, member, shareAmount);
}
}

// If the total intrinsic voting power is not equal to the total voting power,
// it means that the members array is incorrect.
if (totalIntrinsicVotingPower != totalVotingPower) revert WrongMembers();

// Send back any remaining ETH to the sender.
uint256 remainingAmount = address(this).balance;
if (remainingAmount > 0) {
_transfer(ETH_ADDRESS, msg.sender, remainingAmount);
}

emit Distributed(party, token, members, amount);
}

function _transfer(IERC20 token, address to, uint256 amount) internal {
if (token == ETH_ADDRESS) {
// Do not revert on failure. Set gas to 100k to prevent consuming
// all gas.
to.call{ value: amount, gas: 100_000 }("");
} else {
token.compatTransferFrom(msg.sender, to, amount);
}
}
}
57 changes: 57 additions & 0 deletions contracts/proposals/ProposalExecutionEngine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,63 @@ contract ProposalExecutionEngine is
return 0;
}

/// @notice Get the snapshotted intrinsic voting power for an address at a given timestamp.
/// @param voter The address of the voter.
/// @param timestamp The timestamp to get the voting power at.
/// @param hintIndex The precalculated index for the correct snapshot. Not used if incorrect.
/// @return The intrinsic voting power of the address at the given timestamp.
function getIntrinsicVotingPowerAt(
address voter,
uint40 timestamp,
uint256 hintIndex
) public view returns (uint96) {
PartyGovernance.VotingPowerSnapshot[] storage snaps;

// Derive the storage slot for the voting power snapshots mapping.
bytes32 slotAddress = keccak256(
abi.encode(voter, 7 /* slot for the voting power snapshots mapping */)
);
assembly ("memory-safe") {
snaps.slot := slotAddress
}

// Logic copied from https://github.com/PartyDAO/party-protocol/blob/824538633091ebe97c0a0f38c9a28f09900fe173/contracts/party/PartyGovernance.sol#L904-L924
uint256 snapsLength = snaps.length;
if (snapsLength != 0) {
if (
// Hint is within bounds.
hintIndex < snapsLength &&
// Snapshot is not too recent.
snaps[hintIndex].timestamp <= timestamp &&
// Snapshot is not too old.
(hintIndex == snapsLength - 1 || snaps[hintIndex + 1].timestamp > timestamp)
) {
return snaps[hintIndex].intrinsicVotingPower;
}

// Logic copied from https://github.com/PartyDAO/party-protocol/blob/824538633091ebe97c0a0f38c9a28f09900fe173/contracts/party/PartyGovernance.sol#L427-L450
uint256 high = snapsLength;
uint256 low = 0;
while (low < high) {
uint256 mid = (low + high) / 2;
if (snaps[mid].timestamp > timestamp) {
// Entry is too recent.
high = mid;
} else {
// Entry is older. This is our best guess for now.
low = mid + 1;
}
}
hintIndex = high == 0 ? type(uint256).max : high - 1;

// Check that snapshot was found.
if (hintIndex != type(uint256).max) {
return snaps[hintIndex].intrinsicVotingPower;
}
}
return 0;
}

// Switch statement used to execute the right proposal.
function _execute(
ProposalType pt,
Expand Down
Loading
Loading