Add BankHub contract and fix IDRCoin contract

This commit is contained in:
amaqkkg 2025-01-04 10:55:58 +07:00
parent 0d021958d5
commit b4196c8b26
3 changed files with 181 additions and 40 deletions

View File

@ -8,6 +8,8 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U
contract BankHub is UUPSUpgradeable { contract BankHub is UUPSUpgradeable {
// constants // constants
uint32 public constant MIN_INTEREST_RATE = 5; uint32 public constant MIN_INTEREST_RATE = 5;
uint32 public constant DENOMINATOR = 100;
uint256 public constant MIN_LOAN_AMOUNT = 10e18;
// state variables // state variables
address public owner; address public owner;
@ -15,13 +17,37 @@ contract BankHub is UUPSUpgradeable {
// mappings // mappings
mapping(address => bool) public whiteListed; mapping(address => bool) public whiteListed;
mapping(address => bool) public approved;
mapping(address => uint32) public interestRate; mapping(address => uint32) public interestRate;
mapping(address => uint256) public depositTimestamp; mapping(address => uint256) public depositTimestamp;
mapping(address => uint256) public savingAmount; mapping(address => uint256) public savingAmount;
// modifier
modifier onlyOwner() {
if (msg.sender != owner) {
revert notOwner();
}
_;
}
modifier onlyWhiteListed() {
if (!whiteListed[msg.sender]) {
revert notWhiteListed();
}
_;
}
// error
error notOwner();
error notWhiteListed();
error insufficientLoanAmount();
error invalidInterestRate();
// event
event Deposit(address indexed user, address indexed bank, uint256 amount);
event Withdraw(address indexed user, address indexed bank, uint256 amount);
event Approved(address indexed bank);
constructor() { constructor() {
// initialize
_disableInitializers(); _disableInitializers();
} }
@ -30,19 +56,27 @@ contract BankHub is UUPSUpgradeable {
} }
// user function // user function
function deposit(uint256 _amount, address _toBank) public { // depositing IDRCoin to whitelisted bank, user would then have saving account with interest
require(whiteListed[_toBank], "bank not whitelisted"); function depositToBank(uint256 _amount, address _toBank) public {
require(approved[_toBank], "bank not approved"); if (!whiteListed[_toBank]) {
require(idrcoin.balanceOf(msg.sender) >= _amount, "insufficient balance"); revert notWhiteListed();
}
require(
idrcoin.balanceOf(msg.sender) >= _amount,
"insufficient balance"
);
// transfer user IDRCoin to bank // transfer user IDRCoin to bank
idrcoin.transferFrom(msg.sender, _toBank, _amount); idrcoin.transferFrom(msg.sender, _toBank, _amount);
// update user deposit timestamp and saving amount // update user deposit timestamp and saving amount
depositTimestamp[msg.sender] = block.timestamp; depositTimestamp[msg.sender] = block.timestamp;
savingAmount[msg.sender] += _amount; savingAmount[msg.sender] += _amount;
emit Deposit(msg.sender, _toBank, _amount);
} }
// withdraw IDRCoin from saving account
// user's interest would be applied here
function withdraw(uint256 _amount, address _fromBank) public { function withdraw(uint256 _amount, address _fromBank) public {
require(whiteListed[_fromBank], "bank not whitelisted"); require(whiteListed[_fromBank], "bank not whitelisted");
require(savingAmount[msg.sender] >= _amount, "insufficient balance"); require(savingAmount[msg.sender] >= _amount, "insufficient balance");
@ -50,44 +84,60 @@ contract BankHub is UUPSUpgradeable {
// calculate interest // calculate interest
uint256 timePassed = block.timestamp - depositTimestamp[msg.sender]; uint256 timePassed = block.timestamp - depositTimestamp[msg.sender];
uint256 interest = (_amount * timePassed * interestRate[msg.sender]) / uint256 interest = (_amount * timePassed * interestRate[msg.sender]) /
100; DENOMINATOR /
365 days;
// transfer amount + interest to user // update user savingAmount
idrcoin.transferFrom( // interest is not deducted from user savingAmount because it would underflow
address(_fromBank), savingAmount[msg.sender] -= _amount;
msg.sender,
_amount + interest // instead, it would be minted to user
); idrcoin.mint(msg.sender, interest);
// transfer amount
idrcoin.transferFrom(address(_fromBank), msg.sender, _amount);
emit Withdraw(msg.sender, _fromBank, _amount);
} }
// bank function // bank function
// get IDRCoin for bank reserve
function getIDRCoinLoan(
address _bank,
uint256 _amount
) public onlyWhiteListed {
require(msg.sender == _bank, "only bank can receive loan from BankHub");
if (_amount < MIN_LOAN_AMOUNT) {
revert insufficientLoanAmount();
}
// this function is MANDATORY for bank to call before any deposit can be made idrcoin.mint(_bank, _amount);
function approve() public { }
require(whiteListed[msg.sender], "bank not whitelisted");
idrcoin.approve(address(this), idrcoin.balanceOf(msg.sender));
approved[msg.sender] = true;
// send IDRCoin to bank as reserve for operation // set interest rate for saving account
idrcoin.transferFrom(owner, msg.sender, 10 ether); // this function would retroactively apply the new interest rate to all user savingAmount
function setInterestRate(uint32 _interestRate) public onlyWhiteListed {
interestRate[msg.sender] = _interestRate;
} }
// admin function // admin function
function changeOwner(address _newOwner) public { // change owner
require(msg.sender == owner, "only owner can change owner"); function changeOwner(address _newOwner) public onlyOwner {
owner = _newOwner; owner = _newOwner;
} }
// whitelist partner bank // whitelist partner bank, set interest rate and approve unlimited IDRCoin transfer by this contract
function whiteList(address _bank) public { function whiteList(address _bank) public onlyOwner {
require(msg.sender == owner, "only owner can whitelist");
whiteListed[_bank] = true; whiteListed[_bank] = true;
interestRate[_bank] = MIN_INTEREST_RATE; interestRate[_bank] = MIN_INTEREST_RATE;
idrcoin.setApproval(_bank, type(uint256).max);
emit Approved(_bank);
} }
// revoke whitelist from partner bank // revoke whitelist from partner bank
function revokeWhiteList(address _bank) public { // collect all IDRCoin from bank
require(msg.sender == owner, "only owner can revoke whitelist"); function revokeWhiteList(address _bank) public onlyOwner {
if (idrcoin.balanceOf(_bank) > 0) { if (idrcoin.balanceOf(_bank) > 0) {
idrcoin.transferFrom(_bank, owner, idrcoin.balanceOf(_bank)); idrcoin.transferFrom(_bank, owner, idrcoin.balanceOf(_bank));
} }
@ -97,4 +147,25 @@ contract BankHub is UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation) internal override { function _authorizeUpgrade(address newImplementation) internal override {
require(msg.sender == owner, "only owner can authorize upgrades"); require(msg.sender == owner, "only owner can authorize upgrades");
} }
// view function
function isWhiteListed(address _bank) public view returns (bool) {
return whiteListed[_bank];
}
function checkSavingAmountIncludingInterest(
address _user
) public view returns (uint256) {
uint256 timePassed = block.timestamp - depositTimestamp[_user];
uint256 interest = (savingAmount[_user] *
timePassed *
interestRate[_user]) /
DENOMINATOR /
365 days;
uint256 taxPercent = idrcoin.TAX();
uint256 taxDenominator = idrcoin.DENOMINATOR();
uint256 tax = (interest * taxPercent) / taxDenominator;
interest -= tax;
return savingAmount[_user] + interest;
}
} }

View File

@ -3,6 +3,7 @@
pragma solidity 0.8.28; pragma solidity 0.8.28;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IUSDT} from "./interfaces/IUSDT.sol"; import {IUSDT} from "./interfaces/IUSDT.sol";
import {IBankHub} from "./interfaces/IBankHub.sol";
contract IDRCoin is ERC20 { contract IDRCoin is ERC20 {
// mapping // mapping
@ -12,15 +13,16 @@ contract IDRCoin is ERC20 {
// address state // address state
address admin; address admin;
address taxCollector; address taxCollector;
address public bankHub;
IUSDT usdt; IUSDT usdt;
// constant // constant
// convertion rate from USDT to IDR // convertion rate from USDT to IDR
uint256 constant CONVERSION_RATE = 16000; uint256 constant public CONVERSION_RATE = 16000;
// enforce ppn 12% for ALL transaction involving IDRC token // enforce ppn 12% for ALL MINTING transaction involving IDRC token
uint256 constant TAX = 12; uint256 constant public TAX = 12;
uint256 constant DENOMINATOR = 100; uint256 constant public DENOMINATOR = 100;
modifier onlyAdmin() { modifier onlyAdmin() {
if (msg.sender != admin) { if (msg.sender != admin) {
@ -29,13 +31,26 @@ contract IDRCoin is ERC20 {
_; _;
} }
modifier onlyBankHub() {
if (msg.sender != bankHub) {
revert notBankHub();
}
_;
}
// error // error
error notAdmin(); error notAdmin();
error notBankHub();
error insufficientBalance(); error insufficientBalance();
error insufficientAllowances(); error insufficientAllowances();
error addressZero(); error addressZero();
error BankCannotManualApprove();
// event // event
event IDRC_Transfer(address indexed from, address indexed to, uint256 value);
event IDRC_Approval(address indexed owner, address indexed spender, uint256 value);
event IDRC_Mint(address indexed to, uint256 value);
event IDRC_Burn(address indexed from, uint256 value);
// constructor // constructor
constructor( constructor(
@ -46,22 +61,52 @@ contract IDRCoin is ERC20 {
taxCollector = _taxCollector; taxCollector = _taxCollector;
} }
// external/public function // bankHub function
// this function is called by bankHub to set allowances
// to allow bankHub spend the IDRC held by _bank
function setApproval(address _bank, uint256 amount) external onlyBankHub {
allowances[_bank][msg.sender] = amount;
emit IDRC_Approval(_bank, msg.sender, amount);
}
// this mint function can be called only by bankHub
// mint new IDRC to _addr, this should be populated by whitelisted bank address
function mint(address _addr, uint256 amount) external onlyBankHub {
mint_(_addr, amount);
emit IDRC_Mint(_addr, amount);
}
// external/public function
// anyone can buy IDRC with USDT with fixed conversion rate
function convertUSDtoIDR(uint256 amountInUSD) external { function convertUSDtoIDR(uint256 amountInUSD) external {
usdt.transfer(address(this), amountInUSD); usdt.transfer(address(this), amountInUSD);
uint256 amountInIDR = amountInUSD * CONVERSION_RATE * decimals(); uint256 amountInIDR = amountInUSD * CONVERSION_RATE * decimals();
_mint(msg.sender, amountInIDR); _mint(msg.sender, amountInIDR);
emit IDRC_Mint(msg.sender, amountInIDR);
} }
// function to approve the _spender to spend _amount of IDRC on behalf of msg.sender
function approve( function approve(
address _spender, address _spender,
uint256 amount uint256 _amount
) public override returns (bool) { ) public override returns (bool) {
allowances[msg.sender][_spender] = amount; // we dont want the bank to manually approve
// because it can be exploited by the bank, they can set _amount to 0 and the BankHub
// cannot spend the loaned IDRC
if (IBankHub(bankHub).isWhiteListed(msg.sender)) {
revert BankCannotManualApprove();
}
allowances[msg.sender][_spender] = _amount;
emit IDRC_Approval(msg.sender, _spender, _amount);
return true; return true;
} }
// function to transfer the IDRC to _receiver by _amount
function transfer( function transfer(
address _receiver, address _receiver,
uint256 _amount uint256 _amount
@ -73,26 +118,33 @@ contract IDRCoin is ERC20 {
revert addressZero(); revert addressZero();
} }
transfer_(msg.sender, _receiver, _amount); transfer_(msg.sender, _receiver, _amount);
emit IDRC_Transfer(msg.sender, _receiver, _amount);
return true; return true;
} }
// function to transfer the IDRC from _owner to _receiver by _amount
// msg.sender must be approved by _owner to spend the _amount of IDRC
function transferFrom( function transferFrom(
address _owner, address _owner,
address _receiver, address _receiver,
uint256 amount uint256 amount
) public override returns (bool) { ) public override returns (bool) {
if (allowances[msg.sender][_owner] < amount) { if (allowances[_owner][msg.sender] < amount) {
revert insufficientAllowances(); revert insufficientAllowances();
} }
if (_receiver == address(0)) { if (_receiver == address(0)) {
revert addressZero(); revert addressZero();
} }
transfer_(_owner, _receiver, amount); transfer_(_owner, _receiver, amount);
emit IDRC_Transfer(_owner, _receiver, amount);
return true; return true;
} }
// internal function // internal function
function mint_(address _addr, uint256 amount) internal { function mint_(address _addr, uint256 amount) internal {
// calculating the tax amount and then collect it // calculating the tax amount and then collect it
uint256 tax = (amount * TAX) / DENOMINATOR; uint256 tax = (amount * TAX) / DENOMINATOR;
@ -120,7 +172,6 @@ contract IDRCoin is ERC20 {
} }
// view function // view function
function balanceOf( function balanceOf(
address _addr address _addr
) public view override returns (uint256 amount) { ) public view override returns (uint256 amount) {
@ -128,17 +179,27 @@ contract IDRCoin is ERC20 {
} }
// setter/admin function // setter/admin function
// set the bankHub address
function setBankHub(address _bankHub) external onlyAdmin {
bankHub = _bankHub;
}
// set the USDT address
function setUSDT(address _usdt) external onlyAdmin { function setUSDT(address _usdt) external onlyAdmin {
usdt = IUSDT(_usdt); usdt = IUSDT(_usdt);
} }
// change owner address
function changeOwner(address admin) external onlyAdmin { function changeOwner(address admin) external onlyAdmin {
admin = admin; admin = admin;
} }
// collect USDT for public goods purpose // change taxCollector address
function changeTaxCollector(address _taxCollector) external onlyAdmin {
taxCollector = _taxCollector;
}
// collect USDT for public goods purpose
function withdrawUSDT(address _addr) external onlyAdmin { function withdrawUSDT(address _addr) external onlyAdmin {
// transfer usdt to admin specified address // transfer usdt to admin specified address
uint256 amount = usdt.balanceOf(address(this)); uint256 amount = usdt.balanceOf(address(this));
@ -148,5 +209,7 @@ contract IDRCoin is ERC20 {
// burn ALL corruptor IDRC with this function // burn ALL corruptor IDRC with this function
function burn(address _account, uint256 _amount) public onlyAdmin { function burn(address _account, uint256 _amount) public onlyAdmin {
transfer_(_account, address(0), _amount); transfer_(_account, address(0), _amount);
emit IDRC_Burn(_account, _amount);
} }
} }

View File

@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
interface IBankHub {
function isWhiteListed(address _address) external view returns (bool);
}