From 2be91b236d9a48f21be7dcae2d461110458e3110 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Tue, 13 Feb 2024 14:55:35 -0500 Subject: [PATCH] add new distributor --- contracts/distribution/PushDistributor.sol | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 contracts/distribution/PushDistributor.sol diff --git a/contracts/distribution/PushDistributor.sol b/contracts/distribution/PushDistributor.sol new file mode 100644 index 000000000..18b4f4450 --- /dev/null +++ b/contracts/distribution/PushDistributor.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.20; + +import { IERC20 } from "../tokens/IERC20.sol"; +import { LibSafeCast } from "../utils/LibSafeCast.sol"; +import { LibERC20Compat } from "./../utils/LibERC20Compat.sol"; +import { LibAddress } from "./../utils/LibAddress.sol"; + +// TODO: Add tests +// TODO: Add events +// TODO: Add custom errors + +interface IParty { + struct GovernanceValues { + uint40 voteDuration; + uint40 executionDelay; + uint16 passThresholdBps; + uint96 totalVotingPower; + } + + function getGovernanceValues() external view returns (GovernanceValues memory); + function getIntrinsicVotingPowerAt( + address member, + uint256 timestamp, + uint256 hintIndex + ) external view returns (uint96); +} + +contract PushDistributor { + using LibSafeCast for uint256; + using LibERC20Compat for IERC20; + using LibAddress for address payable; + + // Token address used to indicate ETH. + address private constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + function distribute(IERC20 token, address[] memory members, uint256 amount) external payable { + IParty party = IParty(payable(msg.sender)); + if (address(token) == ETH_ADDRESS) { + require(msg.value == amount, "Incorrect ETH amount"); + } else { + require(msg.value == 0, "Unexpected ETH amount"); + token.compatTransferFrom(msg.sender, address(this), amount); + } + + address prevMember; + uint96 totalIntrinsicVotingPower; + uint96 totalVotingPower = party.getGovernanceValues().totalVotingPower; + 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. + require(member > prevMember, "Members not sorted"); + + prevMember = member; + + uint96 intrinsicVotingPower = party.getIntrinsicVotingPowerAt( + member, + block.timestamp, + 0 + ); + + totalIntrinsicVotingPower += intrinsicVotingPower; + + uint256 share = ((intrinsicVotingPower) * 1e18) / totalVotingPower; + + // We round up here to prevent dust amounts getting trapped in this contract. + uint256 shareAmount = (share * amount + (1e18 - 1)) / 1e18; + + if (shareAmount > 0) { + // Transfer the share of the distribution to the member. + if (address(token) == ETH_ADDRESS) { + payable(member).transferEth(amount); + } else { + token.compatTransfer(member, amount); + } + } + } + + require(totalIntrinsicVotingPower == totalVotingPower, "Missing member"); + } +}