From 6a639711f84cdcd7a6353e80d3ba0dc47a884ec4 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Sun, 20 Apr 2025 18:13:40 +0300 Subject: [PATCH 1/6] feat: refactor Contributions and Loans contracts for improved access control and error handling --- src/Contributions.sol | 29 ++++++++++++------------- src/Loans.sol | 49 +++++++++++++++++++++++++++++++++++++++++-- src/utils/Errors.sol | 9 +++++++- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/Contributions.sol b/src/Contributions.sol index f848ecf..3507e68 100644 --- a/src/Contributions.sol +++ b/src/Contributions.sol @@ -2,29 +2,22 @@ pragma solidity 0.8.24; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IContributions} from "./interfaces/IContributions.sol"; import {Loans} from "./Loans.sol"; import {Errors} from "./utils/Errors.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -contract Contributions is Loans, Ownable, IContributions, AccessControl { - using SafeERC20 for IERC20; +contract Contributions is Loans, Ownable, IContributions { using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; address public factoryContract; address private chamaAdmin; - IERC20 token; EnumerableSet.AddressSet private members; - // Access Control Roles - bytes32 private constant MEMBER_ROLE = 0x829b824e2329e205435d941c9f13baf578548505283d29261236d8e6596d4636; - bytes32 private constant CHAMA_ADMIN_ROLE = 0xbb46d0af9106a86e3cb61ab45bd36f61bb3b468e4db75bf9d14199a518ba3f9a; - - mapping(address member => uint256 amount) private memberToAmountContributed; mapping(address member => Member) private memberData; mapping(address => bool) private allowedTokens; @@ -32,13 +25,15 @@ contract Contributions is Loans, Ownable, IContributions, AccessControl { event MemberHasContributed(address indexed member, uint256 amount, uint256 indexed timestamp); event memberRemovedFromChama(address member); - constructor(address _admin, address _token) Ownable(msg.sender) { + constructor(address _admin, address _token, uint256 _interestRate) + Ownable(msg.sender) + Loans(_token, _interestRate) + { members.add(_admin); Member memory newMember = Member(_admin, 0, block.timestamp); memberData[_admin] = newMember; factoryContract = msg.sender; chamaAdmin = _admin; - token = IERC20(_token); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(CHAMA_ADMIN_ROLE, msg.sender); grantChamaAdminRole(_admin); @@ -62,7 +57,7 @@ contract Contributions is Loans, Ownable, IContributions, AccessControl { function addContribution(uint256 _amount) external override onlyRole(MEMBER_ROLE) { memberToAmountContributed[msg.sender] += _amount; - IERC20(token).safeTransferFrom(msg.sender, address(this), _amount); + token.safeTransferFrom(msg.sender, address(this), _amount); emit MemberHasContributed(msg.sender, _amount, block.timestamp); } @@ -76,10 +71,12 @@ contract Contributions is Loans, Ownable, IContributions, AccessControl { revert Errors.Contributions__zeroAmountProvided(); } uint256 totalContributedAmount = memberToAmountContributed[msg.sender]; - if (_amount > totalContributedAmount) { - revert Errors.Contributions__amountThatCanBeWithdrawnIs(totalContributedAmount); + uint256 availableAmt = totalContributedAmount - contrAmtFrozen[msg.sender]; + + if (_amount > availableAmt) { + revert Errors.Contributions__amountNotAvailable(availableAmt); } - IERC20(token).safeTransfer(msg.sender, _amount); + token.safeTransfer(msg.sender, _amount); } /** diff --git a/src/Loans.sol b/src/Loans.sol index 723d109..e417863 100644 --- a/src/Loans.sol +++ b/src/Loans.sol @@ -3,8 +3,53 @@ pragma solidity 0.8.24; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {Errors} from "./utils/Errors.sol"; -contract Loans is ReentrancyGuard { -// This contract handles loans logic for the chamas +// handle the loans for a chama +contract Loans is ReentrancyGuard, AccessControl { + using SafeERC20 for IERC20; + + // We are going to use USDC by default + IERC20 internal token; + uint256 internal interestRate; + + uint256 internal constant BASIS_POINTS = 10000; + + bytes32 internal constant MEMBER_ROLE = keccak256("MEMBER_ROLE"); + bytes32 internal constant CHAMA_ADMIN_ROLE = keccak256("CHAMA_ADMIN_ROLE"); + + mapping(address member => uint256 amount) internal contrAmtFrozen; + mapping(address member => uint256 amount) internal loanAmtAvailable; + mapping(address member => uint256 amount) internal memberToAmountContributed; + + constructor(address _token, uint256 _interestRate) { + token = IERC20(_token); + if (_interestRate > BASIS_POINTS) revert Errors.Loans_invalidInterestRate(BASIS_POINTS, _interestRate); + interestRate = _interestRate; + } + + function guaranteeLoan(address _member, uint256 _amount) external onlyRole(MEMBER_ROLE) { + if (memberToAmountContributed[_member] < _amount) { + revert Errors.Loans__contrAmtLessLoanAmt(memberToAmountContributed[_member], _amount); + } + contrAmtFrozen[msg.sender] += _amount; + loanAmtAvailable[_member] += _amount; + } + + function takeLoan(uint256 _amount) external onlyRole(MEMBER_ROLE) nonReentrant { + if (_amount > contrAmtFrozen[msg.sender]) { + revert Errors.Loans__loanAmtGreaterThanGuaranteedAmt(_amount, loanAmtAvailable[msg.sender]); + } + loanAmtAvailable[msg.sender] -= _amount; + token.safeTransfer(msg.sender, _amount); + } + + function repayLoan(uint256 _amount) external onlyRole(MEMBER_ROLE) nonReentrant { + // Calculate Interest Rate + // add some more logic and checks here + token.safeTransferFrom(msg.sender, address(this), _amount); + } } diff --git a/src/utils/Errors.sol b/src/utils/Errors.sol index 1c4cca0..e17e857 100644 --- a/src/utils/Errors.sol +++ b/src/utils/Errors.sol @@ -3,6 +3,13 @@ pragma solidity ^0.8.24; library Errors { + /*////////////////////////////////////////////////////////////// + LOANS + //////////////////////////////////////////////////////////////*/ + error Loans_invalidInterestRate(uint256 maxInterestRate, uint256 currentInterestRate); + error Loans__contrAmtLessLoanAmt(uint256 contrAmt, uint256 loanAmt); + error Loans__loanAmtGreaterThanGuaranteedAmt(uint256 loanAmt, uint256 guaranteedAmt); + /*////////////////////////////////////////////////////////////// CHAMA //////////////////////////////////////////////////////////////*/ @@ -22,7 +29,7 @@ library Errors { error Contributions__memberAlreadyInChama(address); error Contributions__zeroAmountProvided(); error Contributions__tokenBalanceMustBeZero(); - error Contributions__amountThatCanBeWithdrawnIs(uint256); + error Contributions__amountNotAvailable(uint256); error Contributions__notMemberInChama(); error Contributions__memberShouldHaveZeroBalance(uint256); error Contributions__zeroAddressProvided(); From 2b93d112598035c309d6bcf74f1106c2c2825dc6 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Mon, 28 Apr 2025 12:22:30 +0300 Subject: [PATCH 2/6] feat: update project configuration files and fix type declaration typo in Chama contract --- .idea/.gitignore | 8 ++++++++ .idea/chamadao-contracts.iml | 9 +++++++++ .idea/dictionaries/project.xml | 7 +++++++ .idea/discord.xml | 14 ++++++++++++++ .idea/inspectionProfiles/Project_Default.xml | 6 ++++++ .idea/modules.xml | 8 ++++++++ .idea/prettier.xml | 6 ++++++ .idea/vcs.xml | 11 +++++++++++ src/Chama.sol | 4 ++-- 9 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/chamadao-contracts.iml create mode 100644 .idea/dictionaries/project.xml create mode 100644 .idea/discord.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/prettier.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/chamadao-contracts.iml b/.idea/chamadao-contracts.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/chamadao-contracts.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..2b4031b --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + openzeppelin + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..912db82 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..dee0d33 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..b0c1c68 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..e907ac2 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Chama.sol b/src/Chama.sol index 8c367a3..8fddb72 100644 --- a/src/Chama.sol +++ b/src/Chama.sol @@ -5,7 +5,7 @@ // Imports // Interfaces, libraries, contracts // Errors -// type declations +// type declarations // State variables // Events // Modifiers @@ -30,7 +30,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract Chama is Ownable { address public factoryAdmin; // The admin of the protocol - IERC20 defaultToken; // This is meant to be usdt/usdc to be decided later + IERC20 public defaultToken; // This is meant to be usdt/usdc to be decided later mapping(string => address) private chamas; mapping(string chamaName => mapping(address chamaAddress => address chamaAdmin)) public chamaAdmin; From 0d27c8713d99c43b924d30bd9a54aa82a8a9f1f6 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Wed, 30 Apr 2025 12:49:33 +0300 Subject: [PATCH 3/6] feat: update IContributions struct and add new error for Contributions library --- src/interfaces/IContributions.sol | 2 +- src/utils/Errors.sol | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/interfaces/IContributions.sol b/src/interfaces/IContributions.sol index 12abb1b..adee11b 100644 --- a/src/interfaces/IContributions.sol +++ b/src/interfaces/IContributions.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.24; interface IContributions { struct Member { address member; - uint256 amount; + uint256 availableAmount; uint256 timestamp; } diff --git a/src/utils/Errors.sol b/src/utils/Errors.sol index e17e857..a91bb36 100644 --- a/src/utils/Errors.sol +++ b/src/utils/Errors.sol @@ -9,6 +9,7 @@ library Errors { error Loans_invalidInterestRate(uint256 maxInterestRate, uint256 currentInterestRate); error Loans__contrAmtLessLoanAmt(uint256 contrAmt, uint256 loanAmt); error Loans__loanAmtGreaterThanGuaranteedAmt(uint256 loanAmt, uint256 guaranteedAmt); + error Loans__repayLoanAmountGreaterThanTotalRepayment(uint256 repaymentAmount, uint256 totalRepaymentAmount); /*////////////////////////////////////////////////////////////// CHAMA @@ -34,4 +35,5 @@ library Errors { error Contributions__memberShouldHaveZeroBalance(uint256); error Contributions__zeroAddressProvided(); error Contributions__notFactoryContract(); + error Contributions__epochNotOver(); } From 2511bfe72160e840006e32af0ec7d948173446bb Mon Sep 17 00:00:00 2001 From: ybtuti Date: Wed, 30 Apr 2025 12:49:53 +0300 Subject: [PATCH 4/6] feat: enhance Loans contract with improved loan management and error handling --- src/Loans.sol | 70 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/src/Loans.sol b/src/Loans.sol index e417863..5bcf27a 100644 --- a/src/Loans.sol +++ b/src/Loans.sol @@ -6,29 +6,50 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Errors} from "./utils/Errors.sol"; // handle the loans for a chama contract Loans is ReentrancyGuard, AccessControl { using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; // We are going to use USDC by default + EnumerableSet.AddressSet internal guarantors; IERC20 internal token; - uint256 internal interestRate; + uint256 loanId; + uint256 currentInterestRate; - uint256 internal constant BASIS_POINTS = 10000; + mapping(address member => uint256 amount) internal contrAmtFrozen; + mapping(address member => mapping(address guarantor => uint256 amount)) internal loanAmtAvailable; + mapping(address member => uint256 amount) internal memberToAmountContributed; + mapping(uint256 loanId => Loan loan) internal loanIdToLoanDet; + uint256 internal constant BASIS_POINTS = 10000; bytes32 internal constant MEMBER_ROLE = keccak256("MEMBER_ROLE"); bytes32 internal constant CHAMA_ADMIN_ROLE = keccak256("CHAMA_ADMIN_ROLE"); - mapping(address member => uint256 amount) internal contrAmtFrozen; - mapping(address member => uint256 amount) internal loanAmtAvailable; - mapping(address member => uint256 amount) internal memberToAmountContributed; + struct Loan { + uint256 amount; + uint256 interestRate; + uint256 startTime; + uint256 deadline; + } + + event LoanTaken( + address indexed member, + uint256 amount, + uint256 interestRate, + uint256 startTime, + uint256 deadline, + uint256 indexed loanId + ); constructor(address _token, uint256 _interestRate) { - token = IERC20(_token); if (_interestRate > BASIS_POINTS) revert Errors.Loans_invalidInterestRate(BASIS_POINTS, _interestRate); - interestRate = _interestRate; + currentInterestRate = _interestRate; + token = IERC20(_token); + loanId = 0; } function guaranteeLoan(address _member, uint256 _amount) external onlyRole(MEMBER_ROLE) { @@ -36,19 +57,42 @@ contract Loans is ReentrancyGuard, AccessControl { revert Errors.Loans__contrAmtLessLoanAmt(memberToAmountContributed[_member], _amount); } contrAmtFrozen[msg.sender] += _amount; - loanAmtAvailable[_member] += _amount; + loanAmtAvailable[_member][msg.sender] += _amount; } - function takeLoan(uint256 _amount) external onlyRole(MEMBER_ROLE) nonReentrant { - if (_amount > contrAmtFrozen[msg.sender]) { - revert Errors.Loans__loanAmtGreaterThanGuaranteedAmt(_amount, loanAmtAvailable[msg.sender]); + function takeLoan(uint256 _amount, uint256 _deadline, address guarantor) + external + onlyRole(MEMBER_ROLE) + nonReentrant + { + if (_amount > loanAmtAvailable[msg.sender][guarantor]) { + revert Errors.Loans__loanAmtGreaterThanGuaranteedAmt(_amount, loanAmtAvailable[msg.sender][guarantor]); } - loanAmtAvailable[msg.sender] -= _amount; + loanIdToLoanDet[loanId] = + Loan({amount: _amount, interestRate: currentInterestRate, startTime: block.timestamp, deadline: _deadline}); + loanAmtAvailable[msg.sender][guarantor] -= _amount; token.safeTransfer(msg.sender, _amount); + emit LoanTaken(msg.sender, _amount, currentInterestRate, block.timestamp, _deadline, loanId); + loanId++; } - function repayLoan(uint256 _amount) external onlyRole(MEMBER_ROLE) nonReentrant { + function repayLoan(uint256 _amount, uint256 _loanId, address _guarantor) + external + onlyRole(MEMBER_ROLE) + nonReentrant + { + Loan memory loan = loanIdToLoanDet[_loanId]; // Calculate Interest Rate + uint256 interestAccrued = (loan.amount * loan.interestRate) / BASIS_POINTS; + uint256 totalRepayment = loan.amount + interestAccrued; + if (_amount < totalRepayment) { + loanIdToLoanDet[loanId].amount -= _amount; + } else if (_amount == totalRepayment) { + delete loanIdToLoanDet[loanId]; + } else { + revert Errors.Loans__repayLoanAmountGreaterThanTotalRepayment(_amount, totalRepayment); + } + contrAmtFrozen[_guarantor] -= _amount; // add some more logic and checks here token.safeTransferFrom(msg.sender, address(this), _amount); } From 5d4dd8b4201a32d53267e80aa5edb2d859c2c2ae Mon Sep 17 00:00:00 2001 From: ybtuti Date: Wed, 30 Apr 2025 12:50:01 +0300 Subject: [PATCH 5/6] feat: update createChama function to include interest rate and enhance Contributions contract with epoch management --- src/Chama.sol | 4 ++-- src/Contributions.sol | 29 ++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Chama.sol b/src/Chama.sol index 8fddb72..6a47fd0 100644 --- a/src/Chama.sol +++ b/src/Chama.sol @@ -59,8 +59,8 @@ contract Chama is Ownable { * @param _name name of the new chama */ - function createChama(address _admin, string memory _name) external returns (address) { - Contributions contributions = new Contributions(_admin, address(defaultToken)); + function createChama(address _admin, string memory _name, uint256 _interestRate) external returns (address) { + Contributions contributions = new Contributions(_admin, address(defaultToken), _interestRate); // Effects chamas[_name] = address(contributions); chamaAdmin[_name][address(contributions)] = _admin; diff --git a/src/Contributions.sol b/src/Contributions.sol index 3507e68..2934eb6 100644 --- a/src/Contributions.sol +++ b/src/Contributions.sol @@ -16,6 +16,8 @@ contract Contributions is Loans, Ownable, IContributions { address public factoryContract; address private chamaAdmin; + uint256 public epochPeriod = 30 days; + uint256 public epochEndTime; EnumerableSet.AddressSet private members; mapping(address member => Member) private memberData; @@ -35,6 +37,7 @@ contract Contributions is Loans, Ownable, IContributions { factoryContract = msg.sender; chamaAdmin = _admin; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + _setRoleAdmin(MEMBER_ROLE, CHAMA_ADMIN_ROLE); _grantRole(CHAMA_ADMIN_ROLE, msg.sender); grantChamaAdminRole(_admin); } @@ -70,12 +73,17 @@ contract Contributions is Loans, Ownable, IContributions { if (_amount == 0) { revert Errors.Contributions__zeroAmountProvided(); } + if (block.timestamp < epochEndTime) { + revert Errors.Contributions__epochNotOver(); + } + uint256 totalContributedAmount = memberToAmountContributed[msg.sender]; uint256 availableAmt = totalContributedAmount - contrAmtFrozen[msg.sender]; if (_amount > availableAmt) { revert Errors.Contributions__amountNotAvailable(availableAmt); } + totalContributedAmount -= _amount; token.safeTransfer(msg.sender, _amount); } @@ -83,12 +91,12 @@ contract Contributions is Loans, Ownable, IContributions { * @notice Whitelist a token to be used for contributions * @notice Contract is meant to handle only USDT for now */ - function getContributions(address _member) external view returns (uint256) { - return (memberToAmountContributed[_member]); - } - function calculatePenalties(address _member) external returns (uint256) {} + /*////////////////////////////////////////////////////////////// + ADMIN ROLES + //////////////////////////////////////////////////////////////*/ + function addMemberToChama(address _address) external onlyRole(CHAMA_ADMIN_ROLE) { // Add a member to the chama // Should check if the member is already in the chama @@ -98,7 +106,6 @@ contract Contributions is Loans, Ownable, IContributions { members.add(_address); Member memory newMember = Member(_address, 0, block.timestamp); memberData[_address] = newMember; - grantMemberRole(_address); } function changeAdmin(address _newAdmin) external { @@ -118,6 +125,10 @@ contract Contributions is Loans, Ownable, IContributions { emit TokenHasBeenWhitelisted(_token); } + function setEpochPeriod(uint256 _epochPeriod) external onlyRole(CHAMA_ADMIN_ROLE) { + epochPeriod = _epochPeriod; + } + function removeMemberFromChama(address _member) external onlyRole(CHAMA_ADMIN_ROLE) { // Check if _member is a member of the chama if (!members.contains(_member)) { @@ -156,10 +167,6 @@ contract Contributions is Loans, Ownable, IContributions { _grantRole(CHAMA_ADMIN_ROLE, _admin); } - function grantMemberRole(address _member) public onlyRole(CHAMA_ADMIN_ROLE) { - _grantRole(MEMBER_ROLE, _member); - } - /*////////////////////////////////////////////////////////////// GETTER FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -179,4 +186,8 @@ contract Contributions is Loans, Ownable, IContributions { function getMemberContributions(address _address) external view returns (Member memory) { return memberData[_address]; } + + function getContributions(address _member) external view returns (uint256) { + return (memberToAmountContributed[_member]); + } } From ccc49ac9c1cf5a38db53146c1f2d7fe74f711ba7 Mon Sep 17 00:00:00 2001 From: ybtuti Date: Sun, 11 May 2025 12:50:02 +0300 Subject: [PATCH 6/6] feat: enhance Contributions and Loans contracts with round management and member contribution tracking --- .gitmodules | 6 +++++ src/Contributions.sol | 40 +++++++++++++++++++++---------- src/Loans.sol | 1 + src/interfaces/IContributions.sol | 2 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.gitmodules b/.gitmodules index 27aa4d9..da3645e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std.git +[submodule "lib/v4-core"] + path = lib/v4-core + url = https://github.com/Uniswap/v4-core +[submodule "lib/v4-periphery"] + path = lib/v4-periphery + url = https://github.com/Uniswap/v4-periphery diff --git a/src/Contributions.sol b/src/Contributions.sol index 2934eb6..818e101 100644 --- a/src/Contributions.sol +++ b/src/Contributions.sol @@ -18,14 +18,18 @@ contract Contributions is Loans, Ownable, IContributions { address private chamaAdmin; uint256 public epochPeriod = 30 days; uint256 public epochEndTime; + uint256 public currentRound; EnumerableSet.AddressSet private members; mapping(address member => Member) private memberData; mapping(address => bool) private allowedTokens; + mapping(address => uint256) private memberToRoundClaimed; event TokenHasBeenWhitelisted(address token); event MemberHasContributed(address indexed member, uint256 amount, uint256 indexed timestamp); event memberRemovedFromChama(address member); + event memberHasNoContributions(address member); + event RoundClaimed(address indexed member, uint256 indexed timestamp, uint256 round); constructor(address _admin, address _token, uint256 _interestRate) Ownable(msg.sender) @@ -36,10 +40,11 @@ contract Contributions is Loans, Ownable, IContributions { memberData[_admin] = newMember; factoryContract = msg.sender; chamaAdmin = _admin; + currentRound = 0; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _setRoleAdmin(MEMBER_ROLE, CHAMA_ADMIN_ROLE); _grantRole(CHAMA_ADMIN_ROLE, msg.sender); - grantChamaAdminRole(_admin); + _grantRole(CHAMA_ADMIN_ROLE, _admin); } /*////////////////////////////////////////////////////////////// @@ -65,26 +70,35 @@ contract Contributions is Loans, Ownable, IContributions { emit MemberHasContributed(msg.sender, _amount, block.timestamp); } - function claimRound(uint256 _amount) external nonReentrant onlyRole(MEMBER_ROLE) { + /** + * + * @notice This allow a member to claim the round for all the members in the chama + * @notice This should be called after the epoch period has ended + * @notice This is a design choice, that we allow one of the members to trigger the claim function for all the members + */ + function claimRound() external nonReentrant onlyRole(MEMBER_ROLE) { // Should check whether the member has contributed and also if they are due to claim their round // Should also check if the member has any penalties // Then allow if all checks pass, allow them to claim their round // q should we clear the member's contributions after they claim their round? - if (_amount == 0) { - revert Errors.Contributions__zeroAmountProvided(); - } + if (block.timestamp < epochEndTime) { revert Errors.Contributions__epochNotOver(); } - - uint256 totalContributedAmount = memberToAmountContributed[msg.sender]; - uint256 availableAmt = totalContributedAmount - contrAmtFrozen[msg.sender]; - - if (_amount > availableAmt) { - revert Errors.Contributions__amountNotAvailable(availableAmt); + for (uint256 i = 0; i < members.length(); i++) { + address member = members.at(i); + uint256 contrAmt = memberToAmountContributed[member]; + if (contrAmt == 0) { + emit memberHasNoContributions(member); + continue; + } + uint256 totalContributedAmount = memberToAmountContributed[member]; + uint256 availableAmt = totalContributedAmount - contrAmtFrozen[member]; + token.safeTransfer(msg.sender, availableAmt); } - totalContributedAmount -= _amount; - token.safeTransfer(msg.sender, _amount); + epochEndTime = block.timestamp + epochPeriod; + emit RoundClaimed(msg.sender, block.timestamp, currentRound); + currentRound++; } /** diff --git a/src/Loans.sol b/src/Loans.sol index 5bcf27a..40786b4 100644 --- a/src/Loans.sol +++ b/src/Loans.sol @@ -24,6 +24,7 @@ contract Loans is ReentrancyGuard, AccessControl { mapping(address member => mapping(address guarantor => uint256 amount)) internal loanAmtAvailable; mapping(address member => uint256 amount) internal memberToAmountContributed; mapping(uint256 loanId => Loan loan) internal loanIdToLoanDet; + mapping(address member => uint256 loanId) internal memberToLoanId; uint256 internal constant BASIS_POINTS = 10000; bytes32 internal constant MEMBER_ROLE = keccak256("MEMBER_ROLE"); diff --git a/src/interfaces/IContributions.sol b/src/interfaces/IContributions.sol index adee11b..d22fa40 100644 --- a/src/interfaces/IContributions.sol +++ b/src/interfaces/IContributions.sol @@ -11,7 +11,7 @@ interface IContributions { function addContribution(uint256 _amount) external; - function claimRound(uint256 amount) external; + function claimRound() external; function getContributions(address member) external view returns (uint256);