Skip to content

Commit

Permalink
Merge pull request #5 from Synaps3Protocol/distributor/randomweight
Browse files Browse the repository at this point in the history
Distributor/randomweight
  • Loading branch information
geolffreym authored Oct 21, 2024
2 parents 0ec8896 + 8cc0109 commit e17f72c
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 20 deletions.
2 changes: 2 additions & 0 deletions contracts/policies/access/SubscriptionPolicy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ contract SubscriptionPolicy is BasePolicy {
function exec(T.Agreement calldata agreement) external onlyRM initialized {
Package memory pkg = packages[agreement.holder];
// we need to be sure the user paid for the total of the price..
if (pkg.subscriptionDuration == 0) revert InvalidExecution("Invalid not existing subscription");
if (agreement.total < pkg.price) revert InvalidExecution("Insufficient funds for subscription");

uint256 subTime = block.timestamp + pkg.subscriptionDuration;
// subscribe to content owner's catalog (content package)
subscriptions[agreement.account][agreement.holder] = subTime;
Expand Down
1 change: 1 addition & 0 deletions contracts/rightsmanager/RightsAccessAgreement.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ contract RightsAccessAgreement is Initializable, UUPSUpgradeable, GovernableUpgr
uint256 available = total - deductions; // the total after fees
// one agreement it's unique and cannot be reconstructed..
// create a new immutable agreement to interact with register policy
// agreements aim to serve as attestation..
T.Agreement memory agreement = T.Agreement(
block.timestamp, // the agreement creation time
total, // the transaction total amount
Expand Down
67 changes: 62 additions & 5 deletions contracts/rightsmanager/RightsContentCustodian.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import { GovernableUpgradeable } from "contracts/base/upgradeable/GovernableUpgr
import { IDistributorVerifiable } from "contracts/interfaces/syndication/IDistributorVerifiable.sol";
import { IRightsContentCustodian } from "contracts/interfaces/rightsmanager/IRightsContentCustodian.sol";

import { C } from "contracts/libraries/Constants.sol";

contract RightsContentCustodian is Initializable, UUPSUpgradeable, GovernableUpgradeable, IRightsContentCustodian {
using EnumerableSet for EnumerableSet.AddressSet;

/// Preventing accidental/malicious changes during contract reinitializations.
IDistributorVerifiable public immutable DISTRIBUTOR_REFERENDUM;

/// @dev the max allowed amount of distributors per holder.
uint256 private maxDistributionRedundancy;
/// @dev Mapping to store the custodial address for each content rights holder.
mapping(address => EnumerableSet.AddressSet) private custodying;
/// @dev Mapping to store a registry of rights holders associated with each distributor.
Expand All @@ -25,6 +30,7 @@ contract RightsContentCustodian is Initializable, UUPSUpgradeable, GovernableUpg
event CustodialGranted(address indexed newCustody, address indexed rightsHolder);
/// @dev Error that is thrown when a content hash is already registered.
error InvalidInactiveDistributor();
error MaxRedundancyAllowedReached();

/// @notice Modifier to check if the distributor is active and not blocked.
/// @param distributor The distributor address to check.
Expand All @@ -47,6 +53,11 @@ contract RightsContentCustodian is Initializable, UUPSUpgradeable, GovernableUpg
function initialize() public initializer {
__UUPSUpgradeable_init();
__Governable_init(msg.sender);
// the max amount of distributors per holder..
// we can use this attribute to control de "stress" in the network
// eg: if the network is growing we can adjust this attribute to allow more
// redundancy and more backend distributors..
maxDistributionRedundancy = 3;
}

/// @notice Grants custodial rights over the content held by a holder to a distributor.
Expand All @@ -55,7 +66,10 @@ contract RightsContentCustodian is Initializable, UUPSUpgradeable, GovernableUpg
/// @param distributor The address of the distributor who will receive custodial rights.
function grantCustody(address distributor) external onlyActiveDistributor(distributor) {
// msg.sender expected to be the holder declaring his/her content custody..
// if it's first custody assignment prev = address(0)
if (custodying[msg.sender].length() >= maxDistributionRedundancy) {
revert MaxRedundancyAllowedReached();
}

custodying[msg.sender].add(distributor);
registry[distributor].add(msg.sender);
emit CustodialGranted(distributor, msg.sender);
Expand All @@ -80,13 +94,56 @@ contract RightsContentCustodian is Initializable, UUPSUpgradeable, GovernableUpg
return registry[distributor].values();
}

/// @notice Selects a balanced custodian for a given content rights holder based on weighted randomness.
/// @dev This function behaves similarly to a load balancer in a network proxy system, where each custodian
/// acts like a server, and the function balances the requests (custody assignments) based on a weighted
/// probability distribution. Custodians with higher weights have a greater chance of being selected, much
/// like how a load balancer directs more traffic to servers with greater capacity.
///
/// The randomness used here is not cryptographically secure, but sufficient for this non-critical operation.
/// The random number is generated using the block hash and the sender's address, and is used to determine
/// which custodian is selected.
/// @param holder The address of the content rights holder whose custodian is to be selected.
/// @return choosen The address of the selected custodian.
function getBalancedCustodian(address holder) public view returns (address choosen) {
uint256 i = 0;
uint256 acc = 0;
bytes32 blockHash = blockhash(block.number - 1);
uint256 random = uint256(keccak256(abi.encodePacked(blockHash, msg.sender))) % C.BPS_MAX;
uint256 n = custodying[holder].length();
// arithmetic sucesion
// eg: 3 = 1+2+3 = n(n+1) / 2 = 6
uint256 s = (n * (n + 1)) / 2;

while (i < n) {
// Calculate the weight for each node based on its index (n - i), where the first node gets
// the highest weight, and the weights decrease as i increases.
// We multiply by BPS_MAX (usually 10,000 bps = 100%) to ensure precision, and divide by
// the total weight sum 's' to normalize.
// Formula: w = ((n - i) * BPS_MAX) / s
//
// In a categorical probability distribution, nodes with higher weights have a greater chance
// of being selected. The random value is checked against the cumulative weight.
// Example distribution:
// |------------50------------|--------30--------|-----20------|
// The first node (50%) has the highest chance, followed by the second (30%) and the third (20%).
// += weight for node i
acc += (((n - i) * C.BPS_MAX) / s);

if (acc >= random) {
choosen = custodying[holder].at(i);
}

// i can't overflow n
unchecked {
i++;
}
}
}

/// @notice Retrieves the custodians' addresses for a given content holder.
/// @param holder The address of the content rights holder whose custodians' addresses are being retrieved.
function getCustodians(address holder) public view returns (address[] memory) {
// TODO collect the custody based on demand, round robin?
// TODO un metodo que haga un check o seleccion del custodio disponible.
// TODO VRF generation to select the next custodian?
// TODO if custodians are blocked we need an auxiliar mechanism and return the higher rated distributor
return custodying[holder].values();
}

Expand Down
22 changes: 7 additions & 15 deletions contracts/rightsmanager/RightsPolicyManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -127,29 +127,21 @@ contract RightsPolicyManager is
if (!RIGHTS_AUTHORIZER.isPolicyAuthorized(policyAddress, a7t.holder))
revert InvalidNotRightsDelegated(policyAddress, a7t.holder);

// deposit the total amount to contract during policy registration..
// the available amount is registerd to policy to later allow withdrawals..
// IMPORTANT: the process of distribution registration to accounts should be done in policies logic.
// Deposits the total amount into the contract during policy registration.
// The available amount is registered to the policy to enable future withdrawals.
// IMPORTANT: The process of distributing funds to accounts should be handled within the policy logic.
msg.sender.safeDeposit(a7t.total, a7t.currency);
// validate policy execution register funds and access policy..
try IPolicy(policyAddress).exec(a7t) {
// if-only-if policy execution is successful
_sumLedgerEntry(policyAddress, a7t.available, a7t.currency);
_registerPolicy(a7t.account, policyAddress);
emit AccessGranted(a7t.account, proof, policyAddress);
} catch Error(string memory reason) {
// catch revert with a reason string argument..
// revert(string) and require(false, “reason”)
revert InvalidPolicyRegistration(reason);
} catch (bytes memory custom) {
// still we don't have a custom error catch to handle this
// and we need a way to inform the explicit reason why the policy execution failed
// https://github.com/ethereum/solidity/issues/11278
bytes4 expectedCustom = bytes4(custom);
bytes4 execError = bytes4(keccak256("InvalidExecution(string)"));
bytes4 setupError = bytes4(keccak256("InvalidSetup(string)"));
// only if setup or execution error is the returned.
if (execError == expectedCustom || setupError == expectedCustom) {
// We currently don't have a custom error handler to manage this scenario.
// We need a way to explicitly convey the reason why the policy execution failed.
// Refer to the Solidity GitHub issue: https://github.com/ethereum/solidity/issues/11278
if (bytes4(keccak256("InvalidExecution(string)")) == bytes4(custom)) {
(, string memory reason) = abi.decode(custom, (bytes4, string));
revert InvalidPolicyRegistration(reason);
}
Expand Down
110 changes: 110 additions & 0 deletions test/assets/ContentVault.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
pragma solidity 0.8.26;

import "forge-std/Test.sol";
import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol";
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { IDistributor } from "contracts/interfaces/syndication/IDistributor.sol";
import { IBalanceVerifiable } from "contracts/interfaces/IBalanceVerifiable.sol";
import { IBalanceWithdrawable } from "contracts/interfaces/IBalanceWithdrawable.sol";
import { BaseTest } from "test/BaseTest.t.sol";

contract DistributorImplTest is BaseTest {
address token;

function setUp() public {
token = deployToken();
}

function test_Create_ValidDistributor() public {
address distributor = deployDistributor("test.com");
assertEq(IERC165(distributor).supportsInterface(type(IDistributor).interfaceId), true);
}

function test_GetOwner_ExpectedDeployer() public {
vm.prank(admin);
address distributor = deployDistributor("test2.com");
assertEq(IDistributor(distributor).getManager(), admin);
}

function test_GetEndpoint_ExpectedEndpoint() public {
address distributor = deployDistributor("test3.com");
assertEq(IDistributor(distributor).getEndpoint(), "test3.com");
}

function test_SetEndpoint_ValidEndpoint() public {
// created with an initial endpoint
address distributor = deployDistributor("1.1.1.1");
// changed to a dns domain
vm.prank(admin); // only owner can do this
IDistributor(distributor).setEndpoint("mynew.com");
assertEq(IDistributor(distributor).getEndpoint(), "mynew.com");
}

function test_SetEndpoint_RevertWhen_InvalidOwner() public {
// created with an initial endpoint
address distributor = deployDistributor("1.1.1.1");
vm.prank(user);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", user));
IDistributor(distributor).setEndpoint("mynew.com");
}

function test_GetBalance_ValidBalance() public {
// created with an initial endpoint
address distributor = deployDistributor("1.1.1.1");
uint256 expected = 100 * 1e18;
// admin acting as reward system to transfer funds
// here the expected is that rewards system do it.
vm.startPrank(admin); // only owner can get balance by default deployer
IERC20(token).transfer(distributor, expected);
assertEq(IBalanceVerifiable(distributor).getBalance(token), expected);
vm.stopPrank();
}

function test_Withdraw_ValidFundsWitdrawn() public {
// created with an initial endpoint
uint256 expected = 100 * 1e18;
address distributor = deployDistributor("1.1.1.1");

vm.startPrank(admin); // only owner can get balance by default deployer
IERC20(token).transfer(distributor, expected);
// only owner can withdraw funds by default deployer
IBalanceWithdrawable(distributor).withdraw(user, expected, token);
vm.stopPrank();

assertEq(IERC20(token).balanceOf(user), expected);

}

function test_Withdraw_EmitFundsWithdrawn() public {
// created with an initial endpoint
uint256 expected = 100 * 1e18;
address distributor = deployDistributor("1.1.1.1");

vm.startPrank(admin); // only owner can get balance by default deployer
IERC20(token).transfer(distributor, expected);
// only owner can withdraw funds by default deployer
vm.expectEmit(true, true, false, true, address(distributor));
emit IBalanceWithdrawable.FundsWithdrawn(user, expected, token);
IBalanceWithdrawable(distributor).withdraw(user, expected, token);
}

function test_Withdraw_RevertWhen_NoBalance() public {
// created with an initial endpoint
uint256 expected = 100 * 1e18;
address distributor = deployDistributor("1.1.1.1");

vm.startPrank(admin); // only owner can get balance by default deployer
vm.expectRevert(abi.encodeWithSignature("NoFundsToWithdraw()"));
IBalanceWithdrawable(distributor).withdraw(user, expected, token);
}

function test_Withdraw_RevertWhen_InvalidOwner() public {
// created with an initial endpoint
uint256 expected = 100 * 1e18;
address distributor = deployDistributor("1.1.1.1");

vm.prank(user);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", user));
IBalanceWithdrawable(distributor).withdraw(user, expected, token);
}
}
63 changes: 63 additions & 0 deletions test/assets/ContentVault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import hre from 'hardhat'
import { expect } from 'chai'
import { switcher, getAccounts } from './helpers/CommonHelper'
import { ContentVault, Ownership, Referendum } from '@/typechain-types'
import {
deployInitializedContentVault
} from './helpers/ContentVaultHelper'



describe('ContentVault', function () {

const tests: { id: number, name: string, data: string }[] = [
// example storing LIT in format: chain.ciphertext.dataToEncryptHash
// This data is needed in decryption process and obtained during encryption.
// https://developer.litprotocol.com/sdk/access-control/quick-start#encryption
{ id: 10, name: 'LIT', data: "ethereum.dGVzdF9zZWN1cmVMb25nQ29udGVudF93YXRjaGl0X3Byb3RvY29sX3ZhdWx0c3RvcmFnZQ==.4Kw2DRwEmqK25fOi4bnJ6KLgs8O5gpmxu25VfAUBXXQ=" },
// RSA
{ id: 20, name: 'RSA', data: "8c7e50310a8fc4be1bbadfcd8e9359c8b304dbd96dbe1f5b8ee5b8a249b19fc8403e80aeb2a4b9ebec50fabe98b6e632858571aeb4bde8de2a6471d9a41b1b7c5082d2f2" },
// chachapoly
{ id: 30, name: 'ChaChaPoly', data: "e4bfc4a68d3017c1c50cbb65.f579fc8e4f8e917127cd6d10a85ccbf2.976f64c6011a0a94b9495c1bcf5e7e4ecff4c4e1e9f5293d59b19670f7e945a8a3f1b5045fbe7255ec3d41b5" },
// AES
{ id: 40, name: 'AES', data: "2b2e31fc7c0e47818e18d12b.724f1e73e41f6c5e8bc14a7c6f4d481d.3c4a42b0fcd3e4d5856f8e55b5e8f7e45ac8e58ed8bde3b7d1c58bd74eb3d407ab59b252de04a8c390bbd5" }
];

let referendum: Referendum;
let contentVault: ContentVault;
let ownership: Ownership;

let owner: hre.ethers.HardhatEthersSigner;
let governor: hre.ethers.HardhatEthersSigner;

before(async () => {
[
contentVault,
ownership,
referendum
] = await switcher(deployInitializedContentVault);

[owner, governor] = await getAccounts()
// we need set a governor fake account
await (await referendum.setGovernance(governor)).wait()
// then as "governor account" set a verified role
// to bypass register only approved content..
await (await referendum.connect(governor).grantVerifiedRole(owner)).wait();
})

tests.forEach((test) => {

it(`Should store and retrieve ${test.name} successfully.`, async function () {
// first needed to register content and prove ownership to set secure data
await (await ownership.registerContent(owner, test.id)).wait();
const encoder = hre.ethers.AbiCoder.defaultAbiCoder()
const cypherBytes = encoder.encode(["string"], [test.data])
await contentVault.setContent(test.id, cypherBytes);

const expected = await contentVault.getContent(test.id)
const decoded = encoder.decode(["string"], expected)[0];
expect(decoded).to.be.equal(test.data)
})
})

})

0 comments on commit e17f72c

Please sign in to comment.