Skip to content
This repository was archived by the owner on Aug 14, 2025. It is now read-only.
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
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/chamadao-contracts.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/dictionaries/project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions .idea/discord.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/prettier.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions src/Chama.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Imports
// Interfaces, libraries, contracts
// Errors
// type declations
// type declarations
// State variables
// Events
// Modifiers
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
82 changes: 52 additions & 30 deletions src/Contributions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/*//////////////////////////////////////////////////////////////
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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
//////////////////////////////////////////////////////////////*/
Expand All @@ -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]);
}
}
94 changes: 92 additions & 2 deletions src/Loans.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading
Loading