Skip to content

Commit 2a449e1

Browse files
feat(multi-treasury): add RNSCommission contract and all related scripts (#222)
2 parents 00d638a + 2780c76 commit 2a449e1

13 files changed

+631
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
import { RNSAuction, RNSAuctionDeploy } from "script/contracts/RNSAuctionDeploy.s.sol";
5+
import {
6+
RONRegistrarController, RONRegistrarControllerDeploy
7+
} from "script/contracts/RONRegistrarControllerDeploy.s.sol";
8+
import { Contract } from "script/utils/Contract.sol";
9+
import { RNSCommission, RNSCommissionDeploy } from "script/contracts/RNSCommissionDeploy.s.sol";
10+
import { Migration } from "script/Migration.s.sol";
11+
12+
contract Migration__20240624_MigrateAuctionAndControllerTreasuryAndDeployRNSCommission is Migration {
13+
RNSAuction private _auction;
14+
RONRegistrarController private _controller;
15+
RNSCommission private _rnsCommission;
16+
address private _defaultAdmin;
17+
18+
function run() public {
19+
_controller = RONRegistrarController(loadContract(Contract.RONRegistrarController.key()));
20+
_auction = RNSAuction(loadContract(Contract.RNSAuction.key()));
21+
22+
_rnsCommission = new RNSCommissionDeploy().run();
23+
_defaultAdmin = 0x968D0Cd7343f711216817E617d3f92a23dC91c07;
24+
25+
vm.startBroadcast(_defaultAdmin);
26+
27+
_controller.setTreasury(payable(address(_rnsCommission)));
28+
_auction.setTreasury(payable(address(_rnsCommission)));
29+
30+
vm.stopBroadcast();
31+
}
32+
33+
function _postCheck() internal override {
34+
_validateSendMoneyFromSenders();
35+
_validateTreasuryAddress();
36+
_validateCommissionInfo();
37+
}
38+
39+
function _validateTreasuryAddress() internal logFn("_validateTreasuryAddress") {
40+
address auctionTreasury = _auction.getTreasury();
41+
address controllerTreasury = _controller.getTreasury();
42+
43+
assertEq(auctionTreasury, payable(address(_rnsCommission)));
44+
assertEq(controllerTreasury, payable(address(_rnsCommission)));
45+
}
46+
47+
function _validateCommissionInfo() internal logFn("_validateSetCommissionInfo") {
48+
RNSCommission.Commission[] memory newCommission = new RNSCommission.Commission[](1);
49+
newCommission[0].recipient = payable(makeAddr("Random"));
50+
newCommission[0].ratio = 100_00;
51+
newCommission[0].name = "Random";
52+
53+
vm.prank(_defaultAdmin);
54+
_rnsCommission.setCommissions(newCommission);
55+
56+
assertEq(_rnsCommission.getCommissions().length, 1);
57+
assertEq(_rnsCommission.getCommissions()[0].recipient, newCommission[0].recipient);
58+
}
59+
60+
function _validateSendMoneyFromSenders() internal logFn("_validateSendMoneyFailFromSenders") {
61+
vm.prank(address(_auction));
62+
address(_rnsCommission).call{ value: 100 ether }("");
63+
64+
vm.prank(address(_controller));
65+
address(_rnsCommission).call{ value: 100 ether }("");
66+
67+
assertEq(address(_rnsCommission).balance, 0 ether);
68+
69+
vm.prank(_defaultAdmin);
70+
address(_rnsCommission).call{ value: 100 ether }("");
71+
72+
assertEq(address(_rnsCommission).balance, 100 ether);
73+
}
74+
}

script/GeneralConfig.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ contract GeneralConfig is BaseGeneralConfig {
1717
_mapContractName(Contract.OwnedMulticaller);
1818
_mapContractName(Contract.RNSReverseRegistrar);
1919
_mapContractName(Contract.RONRegistrarController);
20+
_mapContractName(Contract.RNSCommission);
2021
}
2122

2223
function _mapContractName(Contract contractEnum) internal {

script/Migration.s.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ abstract contract Migration is BaseMigration {
7171
param.rnsUnified.protectedSettler = defaultAdmin;
7272
param.rnsUnified.gracePeriod = 90 days;
7373
param.rnsUnified.baseTokenURI = "https://metadata-rns.skymavis.one/saigon/";
74+
75+
// RNSCommission
76+
param.rnsCommission.admin = defaultAdmin;
77+
param.rnsCommission.commissionSetters = new address[](1);
78+
param.rnsCommission.commissionSetters[0] = defaultAdmin;
79+
80+
param.rnsCommission.allowedSenders = new address[](2);
81+
82+
param.rnsCommission.treasuryCommission = new INSCommission.Commission[](2);
83+
param.rnsCommission.treasuryCommission[0].recipient = payable(defaultAdmin);
84+
param.rnsCommission.treasuryCommission[0].ratio = 70_00;
85+
param.rnsCommission.treasuryCommission[0].name = "Sky Mavis";
86+
87+
param.rnsCommission.treasuryCommission[1].recipient = payable(defaultAdmin);
88+
param.rnsCommission.treasuryCommission[1].ratio = 30_00;
89+
param.rnsCommission.treasuryCommission[1].name = "Ronin";
7490
} else if (network() == DefaultNetwork.RoninMainnet.key()) {
7591
address duke = 0x0F68eDBE14C8f68481771016d7E2871d6a35DE11;
7692
address andy = 0xEd4A9F48a62Fb6FdcfB45Bb00C9f61D1A436E58C;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
import { RNSCommission } from "@rns-contracts/RNSCommission.sol";
5+
import { ISharedArgument, Migration } from "script/Migration.s.sol";
6+
import { Contract } from "script/utils/Contract.sol";
7+
import { RONRegistrarController, RONRegistrarControllerDeploy } from "./RONRegistrarControllerDeploy.s.sol";
8+
import { Contract } from "script/utils/Contract.sol";
9+
import { RNSAuction, RNSAuctionDeploy } from "./RNSAuctionDeploy.s.sol";
10+
11+
contract RNSCommissionDeploy is Migration {
12+
function _defaultArguments() internal virtual override returns (bytes memory args) {
13+
ISharedArgument.RNSCommissionParam memory param = config.sharedArguments().rnsCommission;
14+
address[] memory allowedSenders;
15+
allowedSenders = new address[](2);
16+
allowedSenders[0] = param.allowedSenders[0] == address(0)
17+
? address(RNSAuction(loadContract(Contract.RNSAuction.key())))
18+
: param.allowedSenders[0];
19+
allowedSenders[1] = param.allowedSenders[1] == address(0)
20+
? address(RONRegistrarController(loadContract(Contract.RONRegistrarController.key())))
21+
: param.allowedSenders[1];
22+
23+
args = abi.encodeCall(RNSCommission.initialize, (param.admin, param.treasuryCommission, allowedSenders));
24+
}
25+
26+
function run() public virtual returns (RNSCommission) {
27+
return RNSCommission(_deployProxy(Contract.RNSCommission.key()));
28+
}
29+
}

script/interfaces/ISharedArgument.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RNSUnified } from "@rns-contracts/RNSUnified.sol";
1111
import { RNSDomainPrice } from "@rns-contracts/RNSDomainPrice.sol";
1212
import { RNSOperation } from "@rns-contracts/utils/RNSOperation.sol";
1313
import { OwnedMulticaller } from "@rns-contracts/utils/OwnedMulticaller.sol";
14+
import { RNSCommission, INSCommission } from "@rns-contracts/RNSCommission.sol";
1415

1516
interface ISharedArgument is IGeneralConfig {
1617
struct NameCheckerParam {
@@ -86,6 +87,13 @@ interface ISharedArgument is IGeneralConfig {
8687
RNSReverseRegistrar rnsReverseRegistrar;
8788
}
8889

90+
struct RNSCommissionParam {
91+
address admin;
92+
address[] commissionSetters;
93+
INSCommission.Commission[] treasuryCommission;
94+
address[] allowedSenders;
95+
}
96+
8997
struct SharedParameter {
9098
NameCheckerParam nameChecker;
9199
OwnedMulticallerParam ownedMulticaller;
@@ -96,6 +104,7 @@ interface ISharedArgument is IGeneralConfig {
96104
RNSReverseRegistrarParam rnsReverseRegistrar;
97105
RNSUnifiedParam rnsUnified;
98106
RONRegistrarControllerParam ronRegistrarController;
107+
RNSCommissionParam rnsCommission;
99108
}
100109

101110
function sharedArguments() external view returns (SharedParameter memory param);

script/utils/Contract.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ enum Contract {
1313
PublicResolver,
1414
OwnedMulticaller,
1515
RNSReverseRegistrar,
16-
RONRegistrarController
16+
RONRegistrarController,
17+
RNSCommission
1718
}
1819

1920
using { key, name } for Contract global;
@@ -32,5 +33,6 @@ function name(Contract contractEnum) pure returns (string memory) {
3233
if (contractEnum == Contract.OwnedMulticaller) return "OwnedMulticaller";
3334
if (contractEnum == Contract.RNSReverseRegistrar) return "RNSReverseRegistrar";
3435
if (contractEnum == Contract.RONRegistrarController) return "RONRegistrarController";
36+
if (contractEnum == Contract.RNSCommission) return "RNSCommission";
3537
revert("Contract: Unknown contract");
3638
}

src/RNSCommission.sol

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
5+
import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
6+
import { INSCommission } from "./interfaces/INSCommission.sol";
7+
import { RONTransferHelper } from "./libraries/transfers/RONTransferHelper.sol";
8+
9+
contract RNSCommission is Initializable, AccessControlEnumerable, INSCommission {
10+
/// @dev Constant representing the maximum percentage value (100%).
11+
uint256 public constant MAX_PERCENTAGE = 100_00;
12+
/// @dev Role for accounts that can send RON for this contract.
13+
bytes32 public constant SENDER_ROLE = keccak256("SENDER_ROLE");
14+
15+
/// @dev Gap for upgradability.
16+
uint256[50] private ____gap;
17+
/// @dev Array of `Commission` structs that store commissions infomation.
18+
Commission[] internal _commissionInfos;
19+
20+
constructor() {
21+
_disableInitializers();
22+
}
23+
24+
receive() external payable {
25+
_fallback();
26+
}
27+
28+
function initialize(address admin, Commission[] calldata commissionInfos, address[] calldata allowedSenders)
29+
external
30+
initializer
31+
{
32+
_setupRole(DEFAULT_ADMIN_ROLE, admin);
33+
34+
uint256 length = allowedSenders.length;
35+
for (uint256 i; i < length; ++i) {
36+
_setupRole(SENDER_ROLE, allowedSenders[i]);
37+
}
38+
39+
_setCommissions(commissionInfos);
40+
}
41+
42+
/// @inheritdoc INSCommission
43+
function getCommissions() external view returns (Commission[] memory commissionInfos) {
44+
return _commissionInfos;
45+
}
46+
47+
/// @inheritdoc INSCommission
48+
function setCommissions(Commission[] calldata commissionInfos) external onlyRole(DEFAULT_ADMIN_ROLE) {
49+
_setCommissions(commissionInfos);
50+
}
51+
52+
/// @inheritdoc INSCommission
53+
function setCommissionInfo(uint256 commissionIdx, address payable newRecipient, string calldata newName)
54+
external
55+
onlyRole(DEFAULT_ADMIN_ROLE)
56+
{
57+
if (commissionIdx >= _commissionInfos.length) revert InvalidArrayLength();
58+
59+
_commissionInfos[commissionIdx].recipient = newRecipient;
60+
_commissionInfos[commissionIdx].name = newName;
61+
emit CommissionInfoUpdated(msg.sender, commissionIdx, newRecipient, newName);
62+
}
63+
64+
/**
65+
* @dev Helper method to allocate commission and take fee into recipient address.
66+
*/
67+
function _allocateCommissionAndTransferToRecipient(uint256 ronAmount) internal {
68+
if (ronAmount == 0) revert InvalidAmountOfRON();
69+
70+
uint256 length = _commissionInfos.length;
71+
if (length == 0) revert InvalidArrayLength();
72+
73+
uint256 lastIdx = length - 1;
74+
uint256 sumValue;
75+
76+
for (uint256 i; i < lastIdx; ++i) {
77+
uint256 commissionAmount = _computePercentage(ronAmount, _commissionInfos[i].ratio);
78+
sumValue += commissionAmount;
79+
80+
RONTransferHelper.safeTransfer(_commissionInfos[i].recipient, commissionAmount);
81+
emit Distributed(_commissionInfos[i].recipient, commissionAmount);
82+
}
83+
84+
// This code send the remaining RON to the last recipient.
85+
if (sumValue < ronAmount) {
86+
RONTransferHelper.safeTransfer(_commissionInfos[lastIdx].recipient, ronAmount - sumValue);
87+
emit Distributed(_commissionInfos[lastIdx].recipient, ronAmount - sumValue);
88+
}
89+
}
90+
91+
function _setCommissions(Commission[] calldata commissionInfos) internal {
92+
uint256 length = commissionInfos.length;
93+
// commissionInfos[] can not be empty
94+
if (length == 0) revert InvalidArrayLength();
95+
96+
delete _commissionInfos;
97+
98+
uint256 sum;
99+
100+
for (uint256 i; i < length; ++i) {
101+
sum += commissionInfos[i].ratio;
102+
_commissionInfos.push(commissionInfos[i]);
103+
}
104+
105+
if (sum != MAX_PERCENTAGE) revert InvalidRatio();
106+
107+
emit CommissionsUpdated(msg.sender, commissionInfos);
108+
}
109+
110+
// Calculate amount of money based on commission's ratio
111+
function _computePercentage(uint256 value, uint256 percentage) internal pure virtual returns (uint256) {
112+
return (value * percentage) / MAX_PERCENTAGE;
113+
}
114+
115+
function _fallback() internal {
116+
if (hasRole(SENDER_ROLE, msg.sender)) {
117+
_allocateCommissionAndTransferToRecipient(msg.value);
118+
}
119+
}
120+
}

src/interfaces/INSCommission.sol

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
interface INSCommission {
5+
struct Commission {
6+
address payable recipient;
7+
uint256 ratio; // Values [0; 100_00] reflexes [0; 100%]
8+
string name; // Commission's name
9+
}
10+
11+
/// @dev Emitted when all the commission info is updated.
12+
event CommissionsUpdated(address indexed updatedBy, Commission[] commissionInfos);
13+
/// @dev Emitted when specific commission info is updated.
14+
event CommissionInfoUpdated(
15+
address indexed updatedBy, uint256 indexed commissionIdx, address payable newRecipient, string newName
16+
);
17+
/// @dev Emiited when transfer RON to commission's recipient.
18+
event Distributed(address indexed recipient, uint256 commissionAmount);
19+
20+
/// @dev Revert when index is out of range
21+
error InvalidArrayLength();
22+
/// @dev Revert when ratio is invalid
23+
error InvalidRatio();
24+
/// @dev Revert when amount of RON is invalid
25+
error InvalidAmountOfRON();
26+
27+
/**
28+
* @dev Maximum commission percentage.
29+
*/
30+
function MAX_PERCENTAGE() external pure returns (uint256);
31+
32+
/**
33+
* @dev Returns the sender role.
34+
*/
35+
function SENDER_ROLE() external pure returns (bytes32);
36+
37+
/**
38+
* @dev Returns comissions information.
39+
*/
40+
function getCommissions() external view returns (Commission[] memory commissionInfos);
41+
42+
/**
43+
* @dev Sets all commission information
44+
*
45+
* Requirements:
46+
* - The method caller is setter role.
47+
* - The total ratio must be equal to 100%.
48+
* Emits the event `CommissionsUpdated`.
49+
*/
50+
function setCommissions(Commission[] calldata commissionInfos) external;
51+
52+
/**
53+
* @dev Sets for specific commission information based on the `commissionIdx`.
54+
*
55+
* Requirements:
56+
* - The method caller is setter role.
57+
* Emits the event `CommissionInfoUpdated`.
58+
*/
59+
function setCommissionInfo(uint256 commissionIdx, address payable newAddr, string calldata name) external;
60+
}

0 commit comments

Comments
 (0)