diff --git a/src/BankHub.sol b/src/BankHub.sol index eed4792..11bcc66 100644 --- a/src/BankHub.sol +++ b/src/BankHub.sol @@ -8,6 +8,8 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U contract BankHub is UUPSUpgradeable { // constants uint32 public constant MIN_INTEREST_RATE = 5; + uint32 public constant DENOMINATOR = 100; + uint256 public constant MIN_LOAN_AMOUNT = 10e18; // state variables address public owner; @@ -15,13 +17,37 @@ contract BankHub is UUPSUpgradeable { // mappings mapping(address => bool) public whiteListed; - mapping(address => bool) public approved; mapping(address => uint32) public interestRate; mapping(address => uint256) public depositTimestamp; 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() { - // initialize _disableInitializers(); } @@ -30,19 +56,27 @@ contract BankHub is UUPSUpgradeable { } // user function - function deposit(uint256 _amount, address _toBank) public { - require(whiteListed[_toBank], "bank not whitelisted"); - require(approved[_toBank], "bank not approved"); - require(idrcoin.balanceOf(msg.sender) >= _amount, "insufficient balance"); + // depositing IDRCoin to whitelisted bank, user would then have saving account with interest + function depositToBank(uint256 _amount, address _toBank) public { + if (!whiteListed[_toBank]) { + revert notWhiteListed(); + } + require( + idrcoin.balanceOf(msg.sender) >= _amount, + "insufficient balance" + ); // transfer user IDRCoin to bank idrcoin.transferFrom(msg.sender, _toBank, _amount); - + // update user deposit timestamp and saving amount depositTimestamp[msg.sender] = block.timestamp; 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 { require(whiteListed[_fromBank], "bank not whitelisted"); require(savingAmount[msg.sender] >= _amount, "insufficient balance"); @@ -50,44 +84,60 @@ contract BankHub is UUPSUpgradeable { // calculate interest uint256 timePassed = block.timestamp - depositTimestamp[msg.sender]; uint256 interest = (_amount * timePassed * interestRate[msg.sender]) / - 100; + DENOMINATOR / + 365 days; - // transfer amount + interest to user - idrcoin.transferFrom( - address(_fromBank), - msg.sender, - _amount + interest - ); + // update user savingAmount + // interest is not deducted from user savingAmount because it would underflow + savingAmount[msg.sender] -= _amount; + + // 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 - function approve() public { - require(whiteListed[msg.sender], "bank not whitelisted"); - idrcoin.approve(address(this), idrcoin.balanceOf(msg.sender)); - approved[msg.sender] = true; + idrcoin.mint(_bank, _amount); + } - // send IDRCoin to bank as reserve for operation - idrcoin.transferFrom(owner, msg.sender, 10 ether); + // set interest rate for saving account + // 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 - function changeOwner(address _newOwner) public { - require(msg.sender == owner, "only owner can change owner"); + // change owner + function changeOwner(address _newOwner) public onlyOwner { owner = _newOwner; } - // whitelist partner bank - function whiteList(address _bank) public { - require(msg.sender == owner, "only owner can whitelist"); + // whitelist partner bank, set interest rate and approve unlimited IDRCoin transfer by this contract + function whiteList(address _bank) public onlyOwner { whiteListed[_bank] = true; interestRate[_bank] = MIN_INTEREST_RATE; + + idrcoin.setApproval(_bank, type(uint256).max); + emit Approved(_bank); } // revoke whitelist from partner bank - function revokeWhiteList(address _bank) public { - require(msg.sender == owner, "only owner can revoke whitelist"); + // collect all IDRCoin from bank + function revokeWhiteList(address _bank) public onlyOwner { if (idrcoin.balanceOf(_bank) > 0) { idrcoin.transferFrom(_bank, owner, idrcoin.balanceOf(_bank)); } @@ -97,4 +147,25 @@ contract BankHub is UUPSUpgradeable { function _authorizeUpgrade(address newImplementation) internal override { 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; + } } diff --git a/src/IDRCoin.sol b/src/IDRCoin.sol index cc4b596..1001cc0 100644 --- a/src/IDRCoin.sol +++ b/src/IDRCoin.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.28; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IUSDT} from "./interfaces/IUSDT.sol"; +import {IBankHub} from "./interfaces/IBankHub.sol"; contract IDRCoin is ERC20 { // mapping @@ -12,15 +13,16 @@ contract IDRCoin is ERC20 { // address state address admin; address taxCollector; + address public bankHub; IUSDT usdt; // constant // 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 - uint256 constant TAX = 12; - uint256 constant DENOMINATOR = 100; + // enforce ppn 12% for ALL MINTING transaction involving IDRC token + uint256 constant public TAX = 12; + uint256 constant public DENOMINATOR = 100; modifier onlyAdmin() { if (msg.sender != admin) { @@ -29,13 +31,26 @@ contract IDRCoin is ERC20 { _; } + modifier onlyBankHub() { + if (msg.sender != bankHub) { + revert notBankHub(); + } + _; + } + // error error notAdmin(); + error notBankHub(); error insufficientBalance(); error insufficientAllowances(); error addressZero(); + error BankCannotManualApprove(); // 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( @@ -46,22 +61,52 @@ contract IDRCoin is ERC20 { 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 { usdt.transfer(address(this), amountInUSD); uint256 amountInIDR = amountInUSD * CONVERSION_RATE * decimals(); _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( address _spender, - uint256 amount + uint256 _amount ) 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; } + // function to transfer the IDRC to _receiver by _amount function transfer( address _receiver, uint256 _amount @@ -73,26 +118,33 @@ contract IDRCoin is ERC20 { revert addressZero(); } transfer_(msg.sender, _receiver, _amount); + + emit IDRC_Transfer(msg.sender, _receiver, _amount); + 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( address _owner, address _receiver, uint256 amount ) public override returns (bool) { - if (allowances[msg.sender][_owner] < amount) { + if (allowances[_owner][msg.sender] < amount) { revert insufficientAllowances(); } if (_receiver == address(0)) { revert addressZero(); } transfer_(_owner, _receiver, amount); + + emit IDRC_Transfer(_owner, _receiver, amount); + return true; } // internal function - function mint_(address _addr, uint256 amount) internal { // calculating the tax amount and then collect it uint256 tax = (amount * TAX) / DENOMINATOR; @@ -120,7 +172,6 @@ contract IDRCoin is ERC20 { } // view function - function balanceOf( address _addr ) public view override returns (uint256 amount) { @@ -128,17 +179,27 @@ contract IDRCoin is ERC20 { } // 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 { usdt = IUSDT(_usdt); } + // change owner address function changeOwner(address admin) external onlyAdmin { 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 { // transfer usdt to admin specified address uint256 amount = usdt.balanceOf(address(this)); @@ -148,5 +209,7 @@ contract IDRCoin is ERC20 { // burn ALL corruptor IDRC with this function function burn(address _account, uint256 _amount) public onlyAdmin { transfer_(_account, address(0), _amount); + + emit IDRC_Burn(_account, _amount); } } diff --git a/src/interfaces/IBankHub.sol b/src/interfaces/IBankHub.sol new file mode 100644 index 0000000..c7bbdc9 --- /dev/null +++ b/src/interfaces/IBankHub.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.28; + +interface IBankHub { + function isWhiteListed(address _address) external view returns (bool); +}