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

Functionality to withdraw remaining funds post distributions. #30

Merged
merged 6 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion app/blockchain/src/FundingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ contract FundingVault is Ownable, ReentrancyGuard {
error FundingVault__NoVotingPowerTokenMinted();
error FundingVault__TransferFailed();
error FundingVault__AlreadyDistributedFunds();
error FundingVault__FundsNotDistributedYet();
error FundingVault__NoFundsToWithdraw();
error FundingVault__NoRemainingFundsToWithdraw();
error FundingVault__WithdrawableAmountTooSmall();

// Type Declarations //
struct Proposal {
Expand All @@ -78,6 +82,7 @@ contract FundingVault is Ownable, ReentrancyGuard {
uint256 private s_minRequestableAmount;
uint256 private s_maxRequestableAmount;
uint256 private s_totalBalanceAvailableForDistribution;
uint256 private s_totalFundsDistributed;
bool private s_fundsDistributed;

/**
Expand All @@ -89,6 +94,7 @@ contract FundingVault is Ownable, ReentrancyGuard {
mapping(uint256 proposalId => Proposal proposal) private s_proposals;
mapping(uint256 proposalId => uint256 votes) private s_votes;
mapping(address voter => uint256 amountOfVotingTokens) private s_voterToVotingTokens;
mapping(address user => uint256 amountDeposited) private s_userToDistributionAmountDeposited;

// Events //
event FundingTokenDeposited(address indexed from, uint256 indexed amount);
Expand All @@ -97,6 +103,7 @@ contract FundingVault is Ownable, ReentrancyGuard {
event VotedOnProposal(address indexed voter, uint256 indexed proposalId, uint256 indexed amount);
event ReleasedTokens(address indexed voter, uint256 indexed amount);
event FundsDistributed(uint256 indexed proposalId, address indexed recipient, uint256 indexed amount);
event RemainingFundsWithdrawn(address indexed user, uint256 amount);

modifier tallyDatePassed() {
if (block.timestamp < i_tallyDate) {
Expand Down Expand Up @@ -132,6 +139,7 @@ contract FundingVault is Ownable, ReentrancyGuard {
s_minRequestableAmount = _minRequestableAmount;
s_maxRequestableAmount = _maxRequestableAmount;
s_totalBalanceAvailableForDistribution = 0;
s_totalFundsDistributed = 0;
s_fundsDistributed = false;
}

Expand Down Expand Up @@ -167,6 +175,7 @@ contract FundingVault is Ownable, ReentrancyGuard {
}
s_totalBalanceAvailableForDistribution += _amount;
i_fundingToken.transferFrom(msg.sender, address(this), _amount);
s_userToDistributionAmountDeposited[msg.sender] = _amount;
emit FundingTokenDeposited(msg.sender, _amount);
}

Expand Down Expand Up @@ -294,10 +303,10 @@ contract FundingVault is Ownable, ReentrancyGuard {
Proposal memory proposal = s_proposals[i];
if (amount > 0) {
bool success = i_fundingToken.transfer(proposal.recipient, amount);
s_totalBalanceAvailableForDistribution -= amount;
if (!success) {
revert FundingVault__TransferFailed();
}
s_totalFundsDistributed += amount;
emit FundsDistributed(i, proposal.recipient, amount);
}
}
Expand All @@ -317,6 +326,54 @@ contract FundingVault is Ownable, ReentrancyGuard {
emit ReleasedTokens(msg.sender, votingPower);
}

/**
* @notice Allows users to withdraw their proportional share of remaining funds after distribution
* @dev This function can only be called after the tally date has passed and funds have been distributed
* @dev The function calculates the user's share based on their initial deposit and the remaining funds
* @dev State changes are made before the transfer to prevent reentrancy
* @dev Emits a RemainingFundsWithdrawn event upon successful withdrawal
* @dev This function does not take any parameters as it uses msg.sender to identify the user
* @custom:throws FundingVault__FundsNotDistributedYet if funds haven't been distributed yet
* @custom:throws FundingVault__NoFundsToWithdraw if the user has no funds to withdraw
* @custom:throws FundingVault__NoRemainingFundsToWithdraw if there are no remaining funds to withdraw
* @custom:throws FundingVault__WithdrawableAmountTooSmall if the calculated withdrawable amount is zero
* @custom:throws FundingVault__TransferFailed if the token transfer fails
*/
function withdrawRemaining() public nonReentrant tallyDatePassed {
if (!s_fundsDistributed) {
revert FundingVault__FundsNotDistributedYet();
}

uint256 userDepositedAmount = s_userToDistributionAmountDeposited[msg.sender];
if (userDepositedAmount == 0) {
revert FundingVault__NoFundsToWithdraw();
}

uint256 totalDistributableFunds = s_totalBalanceAvailableForDistribution;
uint256 totalDistributedFunds = s_totalFundsDistributed;

if (totalDistributableFunds <= totalDistributedFunds) {
revert FundingVault__NoRemainingFundsToWithdraw();
}

uint256 remainingFunds = totalDistributableFunds - totalDistributedFunds;
uint256 userShareRatio = (userDepositedAmount * 1e18) / totalDistributableFunds;
uint256 userWithdrawableAmount = (userShareRatio * remainingFunds) / 1e18;

if (userWithdrawableAmount == 0) {
revert FundingVault__WithdrawableAmountTooSmall();
}

s_userToDistributionAmountDeposited[msg.sender] = 0;

bool success = i_fundingToken.transfer(msg.sender, userWithdrawableAmount);
if (!success) {
revert FundingVault__TransferFailed();
}

emit RemainingFundsWithdrawn(msg.sender, userWithdrawableAmount);
}

// Getters //
function getProposal(uint256 _proposalId) public view returns (string memory, uint256, uint256, address) {
Proposal memory proposal = s_proposals[_proposalId];
Expand Down Expand Up @@ -367,6 +424,10 @@ contract FundingVault is Ownable, ReentrancyGuard {
return s_totalBalanceAvailableForDistribution;
}

function getTotalFundsDistributed() public view returns (uint256) {
return s_totalFundsDistributed;
}

function getVotingPowerOf(address _voter) public view returns (uint256) {
return s_voterToVotingTokens[_voter];
}
Expand Down
62 changes: 62 additions & 0 deletions app/blockchain/src/mocks/MockFundingVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
error FundingVault__NoVotingPowerTokenMinted();
error FundingVault__TransferFailed();
error FundingVault__AlreadyDistributedFunds();
error FundingVault__FundsNotDistributedYet();
error FundingVault__NoFundsToWithdraw();
error FundingVault__NoRemainingFundsToWithdraw();
error FundingVault__WithdrawableAmountTooSmall();

// Type Declarations //
struct Proposal {
Expand All @@ -78,6 +82,7 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
uint256 private s_minRequestableAmount;
uint256 private s_maxRequestableAmount;
uint256 private s_totalBalanceAvailableForDistribution;
uint256 private s_totalFundsDistributed;
bool private s_fundsDistributed;

/**
Expand All @@ -89,6 +94,7 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
mapping(uint256 proposalId => Proposal proposal) private s_proposals;
mapping(uint256 proposalId => uint256 votes) private s_votes;
mapping(address voter => uint256 amountOfVotingTokens) private s_voterToVotingTokens;
mapping(address user => uint256 amountDeposited) private s_userToDistributionAmountDeposited;

// Events //
event FundingTokenDeposited(address indexed from, uint256 indexed amount);
Expand All @@ -97,6 +103,7 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
event VotedOnProposal(address indexed voter, uint256 indexed proposalId, uint256 indexed amount);
event ReleasedTokens(address indexed voter, uint256 indexed amount);
event FundsDistributed(uint256 indexed proposalId, address indexed recipient, uint256 indexed amount);
event RemainingFundsWithdrawn(address indexed user, uint256 amount);

modifier tallyDatePassed() {
// Will always be true
Expand Down Expand Up @@ -130,6 +137,7 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
s_minRequestableAmount = _minRequestableAmount;
s_maxRequestableAmount = _maxRequestableAmount;
s_totalBalanceAvailableForDistribution = 0;
s_totalFundsDistributed = 0;
s_fundsDistributed = false;
}

Expand Down Expand Up @@ -165,6 +173,7 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
}
s_totalBalanceAvailableForDistribution += _amount;
i_fundingToken.transferFrom(msg.sender, address(this), _amount);
s_userToDistributionAmountDeposited[msg.sender] = _amount;
emit FundingTokenDeposited(msg.sender, _amount);
}

Expand Down Expand Up @@ -295,6 +304,7 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
if (!success) {
revert FundingVault__TransferFailed();
}
s_totalFundsDistributed += amount;
emit FundsDistributed(i, proposal.recipient, amount);
}
}
Expand All @@ -314,6 +324,54 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
emit ReleasedTokens(msg.sender, votingPower);
}

/**
* @notice Allows users to withdraw their proportional share of remaining funds after distribution
* @dev This function can only be called after the tally date has passed and funds have been distributed
* @dev The function calculates the user's share based on their initial deposit and the remaining funds
* @dev State changes are made before the transfer to prevent reentrancy
* @dev Emits a RemainingFundsWithdrawn event upon successful withdrawal
* @dev This function does not take any parameters as it uses msg.sender to identify the user
* @custom:throws FundingVault__FundsNotDistributedYet if funds haven't been distributed yet
* @custom:throws FundingVault__NoFundsToWithdraw if the user has no funds to withdraw
* @custom:throws FundingVault__NoRemainingFundsToWithdraw if there are no remaining funds to withdraw
* @custom:throws FundingVault__WithdrawableAmountTooSmall if the calculated withdrawable amount is zero
* @custom:throws FundingVault__TransferFailed if the token transfer fails
*/
function withdrawRemaining() public nonReentrant tallyDatePassed {
if (!s_fundsDistributed) {
revert FundingVault__FundsNotDistributedYet();
}

uint256 userDepositedAmount = s_userToDistributionAmountDeposited[msg.sender];
if (userDepositedAmount == 0) {
revert FundingVault__NoFundsToWithdraw();
}

uint256 totalDistributableFunds = s_totalBalanceAvailableForDistribution;
uint256 totalDistributedFunds = s_totalFundsDistributed;

if (totalDistributableFunds <= totalDistributedFunds) {
revert FundingVault__NoRemainingFundsToWithdraw();
}

uint256 remainingFunds = totalDistributableFunds - totalDistributedFunds;
uint256 userShareRatio = (userDepositedAmount * 1e18) / totalDistributableFunds;
uint256 userWithdrawableAmount = (userShareRatio * remainingFunds) / 1e18;

if (userWithdrawableAmount == 0) {
revert FundingVault__WithdrawableAmountTooSmall();
}

s_userToDistributionAmountDeposited[msg.sender] = 0;

bool success = i_fundingToken.transfer(msg.sender, userWithdrawableAmount);
if (!success) {
revert FundingVault__TransferFailed();
}

emit RemainingFundsWithdrawn(msg.sender, userWithdrawableAmount);
}

// Getters //
function getProposal(uint256 _proposalId) public view returns (string memory, uint256, uint256, address) {
Proposal memory proposal = s_proposals[_proposalId];
Expand Down Expand Up @@ -364,6 +422,10 @@ contract MockFundingVault is Ownable, ReentrancyGuard {
return s_totalBalanceAvailableForDistribution;
}

function getTotalFundsDistributed() public view returns (uint256) {
return s_totalFundsDistributed;
}

function getVotingPowerOf(address _voter) public view returns (uint256) {
return s_voterToVotingTokens[_voter];
}
Expand Down
Loading