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

153 changes: 153 additions & 0 deletions src/RNSCommission.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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 {
/// @dev Constant representing the maximum percentage value (100%).
uint256 public constant MAX_PERCENTAGE = 100_00;
/// @dev Role for accounts that can set commissions infomation and grant or revoke `SENDER_ROLE`.
bytes32 public constant COMMISSION_SETTER_ROLE = keccak256("COMMISSION_SETTER_ROLE");
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
/// @dev Role for accounts that can send RON for this contract.
bytes32 public constant SENDER_ROLE = keccak256("SENDER_ROLE");
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

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

Commission[] internal _commissionInfos;
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved

constructor() {
_disableInitializers();
}

receive() external payable {
_fallback();
}

fallback() external payable {
_fallback();
}

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) {
_setupRole(SENDER_ROLE, allowedSenders[i]);
}

_setRoleAdmin(SENDER_ROLE, COMMISSION_SETTER_ROLE);
tringuyenskymavis marked this conversation as resolved.
Show resolved Hide resolved
_setTreasuries(treasuryCommission);
}

/// @inheritdoc INSCommission
function getCommissions() external view returns (Commission[] memory treasuriesInfo) {
return _commissionInfos;
}

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

/// @inheritdoc INSCommission
function setTreasuryInfo(uint256 treasuryId, address payable newAddr, bytes calldata name)
external
onlyRole(COMMISSION_SETTER_ROLE)
{
if (treasuryId > _commissionInfos.length - 1) {
revert InvalidArrayLength();
}

_commissionInfos[treasuryId].recipient = newAddr;
_commissionInfos[treasuryId].name = name;
emit TreasuryInfoUpdated(msg.sender, newAddr, name, treasuryId);
}

/**
* @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 = _commissionInfos.length;

allocs = new Allocation[](length);

uint256 lastIdx = length - 1;
uint256 sumValue;

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

// This code replaces value of the last recipient.
if (sumValue < totalAmount) {
allocs[lastIdx] = Allocation({ recipient: _commissionInfos[lastIdx].recipient, value: totalAmount - sumValue });
}
}

/**
* @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 _commissionInfos;

uint256 sum;

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

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

emit TreasuriesUpdated(msg.sender, treasuriesInfo);
}

// 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);
}

function _fallback() internal {
if (hasRole(SENDER_ROLE, msg.sender)) {
_allocateCommissionAndTransferToTreasury(msg.value);
}
}
}
53 changes: 53 additions & 0 deletions src/interfaces/INSCommission.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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(address indexed updatedBy, Commission[] treasuriesInfo);
/// @dev Emitted when specific treasury info are updated.
event TreasuryInfoUpdated(
address indexed updatedBy, address payable treasuryAddr, bytes name, uint256 indexed treasuryId
);

/// @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 comissions information.
*/
function getCommissions() 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 setTreasuryInfo(uint256 treasuryId, address payable newAddr, bytes calldata name) external;
}
Loading