Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multi-treasury transfer contract RNSCommission #217

156 changes: 156 additions & 0 deletions src/RNSCommission.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import { INSCommission } from "./interfaces/INSCommission.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { RONTransferHelper } from "./libraries/transfers/RONTransferHelper.sol";

contract RNSCommission is Initializable, AccessControlEnumerable, INSCommission {
uint256 public constant MAX_PERCENTAGE = 100_00;
bytes32 public constant COMMISSION_SETTER_ROLE = keccak256("COMMISSION_SETTER_ROLE");
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Gap for upgradability.
uint256[50] private ____gap;

Commission[] internal _commissionInfo;
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
mapping(address => bool) public _allowedSenders;
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

constructor() {
_disableInitializers();
}

receive() external payable {
if (_isAllowedSender(msg.sender)) {
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
_allocateCommissionAndTransferToTreasury(msg.value);
}
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
}

fallback() external payable {
if (_isAllowedSender(msg.sender)) {
_allocateCommissionAndTransferToTreasury(msg.value);
}
}
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

function initialize(
address admin,
address[] calldata commissionSetters,
Commission[] calldata treasuryCommission,
address[] calldata allowedSenders
) external initializer {
_setupRole(DEFAULT_ADMIN_ROLE, admin);
uint256 length = commissionSetters.length;
for (uint256 i; i < length; ++i) {
_setupRole(COMMISSION_SETTER_ROLE, commissionSetters[i]);
}

uint256 sendersLength = allowedSenders.length;
for (uint256 i; i < sendersLength; ++i) {
_allowedSenders[allowedSenders[i]] = true;
}

_setTreasuries(treasuryCommission);
}

/// @inheritdoc INSCommission
function getTreasuries() external view returns (Commission[] memory treasuriesInfo) {
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
return _commissionInfo;
}

/// @inheritdoc INSCommission
function setTreasuries(Commission[] calldata treasuriesInfo) external onlyRole(COMMISSION_SETTER_ROLE) {
_setTreasuries(treasuriesInfo);
}

/// @inheritdoc INSCommission
function changeTreasuryInfo(address payable newAddr, bytes calldata name, uint256 treasuryId)
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
external
onlyRole(COMMISSION_SETTER_ROLE)
{
if (treasuryId < 0 || treasuryId > _commissionInfo.length - 1) {
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
revert InvalidArrayLength();
}

_commissionInfo[treasuryId].recipient = newAddr;
_commissionInfo[treasuryId].name = name;
emit TreasuryInfoUpdated(newAddr, name, treasuryId);
}

/// @inheritdoc INSCommission
function allowSender(address sender) external onlyRole(COMMISSION_SETTER_ROLE) {
_allowedSenders[sender] = true;
}
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

/**
* @dev Helper method to calculate allocations.
*/
function _calcAllocations(uint256 totalAmount) internal view returns (Allocation[] memory allocs) {
if (totalAmount == 0) {
revert InvalidAmountOfRON();
}
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
uint256 length = _commissionInfo.length;

allocs = new Allocation[](length);

uint256 lastIdx = length - 1;
uint256 sumValue;

for (uint256 i; i < lastIdx; ++i) {
allocs[i] = Allocation({
recipient: _commissionInfo[i].recipient,
value: _computePercentage(totalAmount, _commissionInfo[i].ratio)
});
sumValue += allocs[i].value;
}

// Refund the remaining RON to the last treasury
if (sumValue < totalAmount) {
allocs[lastIdx] = Allocation({ recipient: _commissionInfo[lastIdx].recipient, value: totalAmount - sumValue });
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* @dev Helper method to allocate commission and take fee into treasuries address.
*/
function _allocateCommissionAndTransferToTreasury(uint256 ronAmount) internal {
INSCommission.Allocation[] memory allocs = _calcAllocations(ronAmount);
uint256 length = allocs.length;

for (uint256 i = 0; i < length; ++i) {
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
uint256 value = allocs[i].value;
address payable recipient = allocs[i].recipient;
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

RONTransferHelper.safeTransfer(recipient, value);
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
}
}

function _setTreasuries(Commission[] calldata treasuriesInfo) internal {
uint256 length = treasuriesInfo.length;
// treasuriesInfo[] can not be empty
if (length < 1) revert InvalidArrayLength();
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

delete _commissionInfo;

uint256 sum = 0;
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

for (uint256 i = 0; i < length; ++i) {
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
sum += treasuriesInfo[i].ratio;
_commissionInfo.push(treasuriesInfo[i]);
}

if (sum != MAX_PERCENTAGE) revert InvalidRatio();

emit TreasuriesUpdated(treasuriesInfo);
}

/// Check if `sender` is allowed to send money
function _isAllowedSender(address sender) internal view returns (bool) {
return _allowedSenders[sender];
}
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

// Calculate amount of money based on treasury's ratio
function _computePercentage(uint256 value, uint256 percentage) internal pure virtual returns (uint256) {
return Math.mulDiv(value, percentage, MAX_PERCENTAGE);
}
}
59 changes: 59 additions & 0 deletions src/interfaces/INSCommission.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

interface INSCommission {
struct Commission {
address payable recipient;
uint256 ratio; // Values [0; 100_00] reflexes [0; 100%]
bytes name; // Treasury's name
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
}

struct Allocation {
address payable recipient;
uint256 value;
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
}

/// @dev Emitted when all the treasury info info are updated.
event TreasuriesUpdated(Commission[] treasuriesInfo);
/// @dev Emitted when specific treasury info are updated.
event TreasuryInfoUpdated(address payable treasuryAddr, bytes name, uint256 treasuryId);
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Revert when index is out of range
error InvalidArrayLength();
/// @dev Revert when ratio is invalid
error InvalidRatio();
/// @dev Revert when amount of RON is invalid
error InvalidAmountOfRON();

/**
* @dev Returns all treasury.
*/
function getTreasuries() external view returns (Commission[] memory treasuriesInfo);

/**
* @dev Sets all treasuries information
*
* Requirements:
* - The method caller is setter role.
* - The total ratio must be equal to 100%.
* Emits the event `TreasuriesUpdated`.
*/
function setTreasuries(Commission[] calldata treasuriesInfo) external;

/**
* @dev Sets for specific treasury information based on the treasury `id`.
*
* Requirements:
* - The method caller is setter role.
* Emits the event `TreasuryInfoUpdated`.
*/
function changeTreasuryInfo(address payable newAddr, bytes calldata name, uint256 treasuryId) external;
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

/**
* @dev Allows specific `sender` to send money to this contract, which will then be transferred to the treasuries.
*
* Requirements:
* - The method caller is setter role.
*/
function allowSender(address sender) external;
}
Loading