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/.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..6a47fd0 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;
@@ -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 f848ecf..818e101 100644
--- a/src/Contributions.sol
+++ b/src/Contributions.sol
@@ -2,46 +2,49 @@
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;
+ uint256 public epochPeriod = 30 days;
+ uint256 public epochEndTime;
+ uint256 public currentRound;
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;
+ 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) 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);
+ 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);
}
/*//////////////////////////////////////////////////////////////
@@ -62,36 +65,52 @@ 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);
}
- 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];
- if (_amount > totalContributedAmount) {
- revert Errors.Contributions__amountThatCanBeWithdrawnIs(totalContributedAmount);
+ 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);
}
- IERC20(token).safeTransfer(msg.sender, _amount);
+ epochEndTime = block.timestamp + epochPeriod;
+ emit RoundClaimed(msg.sender, block.timestamp, currentRound);
+ currentRound++;
}
/**
* @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
@@ -101,7 +120,6 @@ contract Contributions is Loans, Ownable, IContributions, AccessControl {
members.add(_address);
Member memory newMember = Member(_address, 0, block.timestamp);
memberData[_address] = newMember;
- grantMemberRole(_address);
}
function changeAdmin(address _newAdmin) external {
@@ -121,6 +139,10 @@ contract Contributions is Loans, Ownable, IContributions, AccessControl {
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)) {
@@ -159,10 +181,6 @@ contract Contributions is Loans, Ownable, IContributions, AccessControl {
_grantRole(CHAMA_ADMIN_ROLE, _admin);
}
- function grantMemberRole(address _member) public onlyRole(CHAMA_ADMIN_ROLE) {
- _grantRole(MEMBER_ROLE, _member);
- }
-
/*//////////////////////////////////////////////////////////////
GETTER FUNCTIONS
//////////////////////////////////////////////////////////////*/
@@ -182,4 +200,8 @@ contract Contributions is Loans, Ownable, IContributions, AccessControl {
function getMemberContributions(address _address) external view returns (Member memory) {
return memberData[_address];
}
+
+ function getContributions(address _member) external view returns (uint256) {
+ return (memberToAmountContributed[_member]);
+ }
}
diff --git a/src/Loans.sol b/src/Loans.sol
index 723d109..40786b4 100644
--- a/src/Loans.sol
+++ b/src/Loans.sol
@@ -3,8 +3,98 @@
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 {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.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;
+ using EnumerableSet for EnumerableSet.AddressSet;
+
+ // We are going to use USDC by default
+ EnumerableSet.AddressSet internal guarantors;
+ IERC20 internal token;
+ uint256 loanId;
+ uint256 currentInterestRate;
+
+ 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;
+ mapping(address member => uint256 loanId) internal memberToLoanId;
+
+ uint256 internal constant BASIS_POINTS = 10000;
+ bytes32 internal constant MEMBER_ROLE = keccak256("MEMBER_ROLE");
+ bytes32 internal constant CHAMA_ADMIN_ROLE = keccak256("CHAMA_ADMIN_ROLE");
+
+ 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) {
+ if (_interestRate > BASIS_POINTS) revert Errors.Loans_invalidInterestRate(BASIS_POINTS, _interestRate);
+ currentInterestRate = _interestRate;
+ token = IERC20(_token);
+ loanId = 0;
+ }
+
+ 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][msg.sender] += _amount;
+ }
+
+ 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]);
+ }
+ 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, 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);
+ }
}
diff --git a/src/interfaces/IContributions.sol b/src/interfaces/IContributions.sol
index 12abb1b..d22fa40 100644
--- a/src/interfaces/IContributions.sol
+++ b/src/interfaces/IContributions.sol
@@ -5,13 +5,13 @@ pragma solidity 0.8.24;
interface IContributions {
struct Member {
address member;
- uint256 amount;
+ uint256 availableAmount;
uint256 timestamp;
}
function addContribution(uint256 _amount) external;
- function claimRound(uint256 amount) external;
+ function claimRound() external;
function getContributions(address member) external view returns (uint256);
diff --git a/src/utils/Errors.sol b/src/utils/Errors.sol
index 1c4cca0..a91bb36 100644
--- a/src/utils/Errors.sol
+++ b/src/utils/Errors.sol
@@ -3,6 +3,14 @@
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);
+ error Loans__repayLoanAmountGreaterThanTotalRepayment(uint256 repaymentAmount, uint256 totalRepaymentAmount);
+
/*//////////////////////////////////////////////////////////////
CHAMA
//////////////////////////////////////////////////////////////*/
@@ -22,9 +30,10 @@ 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();
error Contributions__notFactoryContract();
+ error Contributions__epochNotOver();
}