Skip to content
Open
93 changes: 93 additions & 0 deletions contracts/distribution/PushDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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 { LibERC20Compat } from "./../utils/LibERC20Compat.sol";
import { ReentrancyGuard } from "openzeppelin/contracts/security/ReentrancyGuard.sol";

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

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

using LibERC20Compat for IERC20;

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

function distribute(

Copy link
Copy Markdown
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 nonReentrant {
Party party = Party(payable(msg.sender));
if (token == ETH_ADDRESS && msg.value < amount) revert NotEnoughETH(amount, msg.value);

Comment thread
0xble marked this conversation as 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
14 changes: 13 additions & 1 deletion deploy/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { SSTORE2MetadataProvider } from "../contracts/renderers/SSTORE2MetadataP
import { BasicMetadataProvider } from "../contracts/renderers/BasicMetadataProvider.sol";
import { OffChainSignatureValidator } from "../contracts/signature-validators/OffChainSignatureValidator.sol";
import { BondingCurveAuthority } from "../contracts/authorities/BondingCurveAuthority.sol";
import { PushDistributor } from "../contracts/distribution/PushDistributor.sol";
import "./LibDeployConstants.sol";

abstract contract Deploy {
Expand Down Expand Up @@ -85,6 +86,7 @@ abstract contract Deploy {
SellPartyCardsAuthority public sellPartyCardsAuthority;
OffChainSignatureValidator public offChainSignatureValidator;
BondingCurveAuthority public bondingCurveAuthority;
PushDistributor public pushDistributor;

function deploy(LibDeployConstants.DeployConstants memory deployConstants) public virtual {
_switchDeployer(DeployerRole.Default);
Expand All @@ -110,6 +112,15 @@ abstract contract Deploy {
console.log(" Deployed - TokenDistributor", address(tokenDistributor));
_switchDeployer(DeployerRole.Default);

// DEPLOY_PUSH_DISTRIBUTOR
console.log("");
console.log("### PushDistributor");
console.log(" Deploying - PushDistributor");
_trackDeployerGasBefore();
pushDistributor = new PushDistributor();
_trackDeployerGasAfter();
console.log(" Deployed - PushDistributor", address(pushDistributor));

// DEPLOY_PROPOSAL_EXECUTION_ENGINE
console.log("");
console.log("### ProposalExecutionEngine");
Expand Down Expand Up @@ -715,7 +726,7 @@ contract DeployScript is Script, Deploy {
Deploy.deploy(deployConstants);
vm.stopBroadcast();

AddressMapping[] memory addressMapping = new AddressMapping[](30);
AddressMapping[] memory addressMapping = new AddressMapping[](31);
addressMapping[0] = AddressMapping("Globals", address(globals));
addressMapping[1] = AddressMapping("TokenDistributor", address(tokenDistributor));
addressMapping[2] = AddressMapping(
Expand Down Expand Up @@ -782,6 +793,7 @@ contract DeployScript is Script, Deploy {
"OffChainSignatureValidator",
address(offChainSignatureValidator)
);
addressMapping[30] = AddressMapping("PushDistributor", address(pushDistributor));

console.log("");
console.log("### Deployed addresses");
Expand Down
Loading