Bläddra i källkod

[entropy] Add Fee Manager Role (#1623)

* initial cut at entropy fee manager

* cleanup

* tests

* update ABIs

* doc

* bump version

* add ABIs
Jayant Krishnamurthy 1 år sedan
förälder
incheckning
587a4a7eb2

+ 1 - 1
package-lock.json

@@ -67485,7 +67485,7 @@
     },
     "target_chains/ethereum/entropy_sdk/solidity": {
       "name": "@pythnetwork/entropy-sdk-solidity",
-      "version": "1.3.0",
+      "version": "1.5.0",
       "license": "Apache-2.0",
       "devDependencies": {
         "abi_generator": "*",

+ 67 - 0
target_chains/ethereum/contracts/contracts/entropy/Entropy.sol

@@ -162,6 +162,38 @@ abstract contract Entropy is IEntropy, EntropyState {
         // Interaction with an external contract or token transfer
         (bool sent, ) = msg.sender.call{value: amount}("");
         require(sent, "withdrawal to msg.sender failed");
+
+        emit Withdrawal(msg.sender, msg.sender, amount);
+    }
+
+    function withdrawAsFeeManager(
+        address provider,
+        uint128 amount
+    ) external override {
+        EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
+            provider
+        ];
+
+        if (providerInfo.sequenceNumber == 0) {
+            revert EntropyErrors.NoSuchProvider();
+        }
+
+        if (providerInfo.feeManager != msg.sender) {
+            revert EntropyErrors.Unauthorized();
+        }
+
+        // Use checks-effects-interactions pattern to prevent reentrancy attacks.
+        require(
+            providerInfo.accruedFeesInWei >= amount,
+            "Insufficient balance"
+        );
+        providerInfo.accruedFeesInWei -= amount;
+
+        // Interaction with an external contract or token transfer
+        (bool sent, ) = msg.sender.call{value: amount}("");
+        require(sent, "withdrawal to msg.sender failed");
+
+        emit Withdrawal(provider, msg.sender, amount);
     }
 
     // requestHelper allocates and returns a new request for the given provider.
@@ -475,6 +507,28 @@ abstract contract Entropy is IEntropy, EntropyState {
         emit ProviderFeeUpdated(msg.sender, oldFeeInWei, newFeeInWei);
     }
 
+    function setProviderFeeAsFeeManager(
+        address provider,
+        uint128 newFeeInWei
+    ) external override {
+        EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
+            provider
+        ];
+
+        if (providerInfo.sequenceNumber == 0) {
+            revert EntropyErrors.NoSuchProvider();
+        }
+
+        if (providerInfo.feeManager != msg.sender) {
+            revert EntropyErrors.Unauthorized();
+        }
+
+        uint128 oldFeeInWei = providerInfo.feeInWei;
+        providerInfo.feeInWei = newFeeInWei;
+
+        emit ProviderFeeUpdated(provider, oldFeeInWei, newFeeInWei);
+    }
+
     // Set provider uri. It will revert if provider is not registered.
     function setProviderUri(bytes calldata newUri) external override {
         EntropyStructs.ProviderInfo storage provider = _state.providers[
@@ -488,6 +542,19 @@ abstract contract Entropy is IEntropy, EntropyState {
         emit ProviderUriUpdated(msg.sender, oldUri, newUri);
     }
 
+    function setFeeManager(address manager) external override {
+        EntropyStructs.ProviderInfo storage provider = _state.providers[
+            msg.sender
+        ];
+        if (provider.sequenceNumber == 0) {
+            revert EntropyErrors.NoSuchProvider();
+        }
+
+        address oldFeeManager = provider.feeManager;
+        provider.feeManager = manager;
+        emit ProviderFeeManagerUpdated(msg.sender, oldFeeManager, manager);
+    }
+
     function constructUserCommitment(
         bytes32 userRandomness
     ) public pure override returns (bytes32 userCommitment) {

+ 46 - 0
target_chains/ethereum/contracts/forge-test/Entropy.t.sol

@@ -926,6 +926,52 @@ contract EntropyTest is Test, EntropyTestUtils, EntropyEvents {
             provider1Proofs[assignedSequenceNumber]
         );
     }
+
+    function testFeeManager() public {
+        address manager = address(12);
+
+        // Make sure there are some fees for provider1
+        request(user1, provider1, 42, false);
+
+        // Manager isn't authorized, so can't withdraw or set fee.
+        vm.prank(manager);
+        vm.expectRevert();
+        random.withdrawAsFeeManager(provider1, provider1FeeInWei);
+        vm.prank(manager);
+        vm.expectRevert();
+        random.setProviderFeeAsFeeManager(provider1, 1000);
+
+        // You can't set a fee manager from an address that isn't a registered provider.
+        vm.expectRevert();
+        random.setFeeManager(address(manager));
+
+        // Authorizing the fee manager as the provider enables withdrawing and setting fees.
+        vm.prank(provider1);
+        random.setFeeManager(address(manager));
+
+        // Withdrawing decrements provider's accrued fees and sends balance to the fee manager.
+        uint startingBalance = manager.balance;
+        vm.prank(manager);
+        random.withdrawAsFeeManager(provider1, provider1FeeInWei);
+        assertEq(random.getProviderInfo(provider1).accruedFeesInWei, 0);
+        assertEq(manager.balance, startingBalance + provider1FeeInWei);
+
+        // Setting provider fee updates the fee in the ProviderInfo.
+        vm.prank(manager);
+        random.setProviderFeeAsFeeManager(provider1, 10101);
+        assertEq(random.getProviderInfo(provider1).feeInWei, 10101);
+
+        // Authorizing a different manager depermissions the previous one.
+        address manager2 = address(13);
+        vm.prank(provider1);
+        random.setFeeManager(address(manager2));
+        vm.prank(manager);
+        vm.expectRevert();
+        random.withdrawAsFeeManager(provider1, provider1FeeInWei);
+        vm.prank(manager);
+        vm.expectRevert();
+        random.setProviderFeeAsFeeManager(provider1, 1000);
+    }
 }
 
 contract EntropyConsumer is IEntropyConsumer {

+ 12 - 0
target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol

@@ -32,4 +32,16 @@ interface EntropyEvents {
     event ProviderFeeUpdated(address provider, uint128 oldFee, uint128 newFee);
 
     event ProviderUriUpdated(address provider, bytes oldUri, bytes newUri);
+
+    event ProviderFeeManagerUpdated(
+        address provider,
+        address oldFeeManager,
+        address newFeeManager
+    );
+
+    event Withdrawal(
+        address provider,
+        address recipient,
+        uint128 withdrawnAmount
+    );
 }

+ 2 - 0
target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol

@@ -32,6 +32,8 @@ contract EntropyStructs {
         // are revealed on-chain.
         bytes32 currentCommitment;
         uint64 currentCommitmentSequenceNumber;
+        // An address that is authorized to set / withdraw fees on behalf of this provider.
+        address feeManager;
     }
 
     struct Request {

+ 16 - 0
target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol

@@ -22,6 +22,11 @@ interface IEntropy is EntropyEvents {
     // balance of fees in the contract).
     function withdraw(uint128 amount) external;
 
+    // Withdraw a portion of the accumulated fees for provider. The msg.sender must be the fee manager for this provider.
+    // Calling this function will transfer `amount` wei to the caller (provided that they have accrued a sufficient
+    // balance of fees in the contract).
+    function withdrawAsFeeManager(address provider, uint128 amount) external;
+
     // As a user, request a random number from `provider`. Prior to calling this method, the user should
     // generate a random number x and keep it secret. The user should then compute hash(x) and pass that
     // as the userCommitment argument. (You may call the constructUserCommitment method to compute the hash.)
@@ -102,8 +107,19 @@ interface IEntropy is EntropyEvents {
 
     function setProviderFee(uint128 newFeeInWei) external;
 
+    function setProviderFeeAsFeeManager(
+        address provider,
+        uint128 newFeeInWei
+    ) external;
+
     function setProviderUri(bytes calldata newUri) external;
 
+    // Set manager as the fee manager for the provider msg.sender.
+    // After calling this function, manager will be able to set the provider's fees and withdraw them.
+    // Only one address can be the fee manager for a provider at a time -- calling this function again with a new value
+    // will override the previous value. Call this function with the all-zero address to disable the fee manager role.
+    function setFeeManager(address manager) external;
+
     function constructUserCommitment(
         bytes32 userRandomness
     ) external pure returns (bytes32 userCommitment);

+ 55 - 0
target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json

@@ -1,4 +1,29 @@
 [
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "oldFeeManager",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "newFeeManager",
+        "type": "address"
+      }
+    ],
+    "name": "ProviderFeeManagerUpdated",
+    "type": "event"
+  },
   {
     "anonymous": false,
     "inputs": [
@@ -103,6 +128,11 @@
             "internalType": "uint64",
             "name": "currentCommitmentSequenceNumber",
             "type": "uint64"
+          },
+          {
+            "internalType": "address",
+            "name": "feeManager",
+            "type": "address"
           }
         ],
         "indexed": false,
@@ -399,5 +429,30 @@
     ],
     "name": "RevealedWithCallback",
     "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "recipient",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint128",
+        "name": "withdrawnAmount",
+        "type": "uint128"
+      }
+    ],
+    "name": "Withdrawal",
+    "type": "event"
   }
 ]

+ 109 - 0
target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json

@@ -1,4 +1,29 @@
 [
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "oldFeeManager",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "newFeeManager",
+        "type": "address"
+      }
+    ],
+    "name": "ProviderFeeManagerUpdated",
+    "type": "event"
+  },
   {
     "anonymous": false,
     "inputs": [
@@ -103,6 +128,11 @@
             "internalType": "uint64",
             "name": "currentCommitmentSequenceNumber",
             "type": "uint64"
+          },
+          {
+            "internalType": "address",
+            "name": "feeManager",
+            "type": "address"
           }
         ],
         "indexed": false,
@@ -400,6 +430,31 @@
     "name": "RevealedWithCallback",
     "type": "event"
   },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "address",
+        "name": "recipient",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint128",
+        "name": "withdrawnAmount",
+        "type": "uint128"
+      }
+    ],
+    "name": "Withdrawal",
+    "type": "event"
+  },
   {
     "inputs": [
       {
@@ -554,6 +609,11 @@
             "internalType": "uint64",
             "name": "currentCommitmentSequenceNumber",
             "type": "uint64"
+          },
+          {
+            "internalType": "address",
+            "name": "feeManager",
+            "type": "address"
           }
         ],
         "internalType": "struct EntropyStructs.ProviderInfo",
@@ -778,6 +838,19 @@
     "stateMutability": "nonpayable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "address",
+        "name": "manager",
+        "type": "address"
+      }
+    ],
+    "name": "setFeeManager",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
   {
     "inputs": [
       {
@@ -791,6 +864,24 @@
     "stateMutability": "nonpayable",
     "type": "function"
   },
+  {
+    "inputs": [
+      {
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "internalType": "uint128",
+        "name": "newFeeInWei",
+        "type": "uint128"
+      }
+    ],
+    "name": "setProviderFeeAsFeeManager",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
   {
     "inputs": [
       {
@@ -816,5 +907,23 @@
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "internalType": "uint128",
+        "name": "amount",
+        "type": "uint128"
+      }
+    ],
+    "name": "withdrawAsFeeManager",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
   }
 ]

+ 1 - 1
target_chains/ethereum/entropy_sdk/solidity/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/entropy-sdk-solidity",
-  "version": "1.3.0",
+  "version": "1.5.0",
   "description": "Generate secure random numbers with Pyth Entropy",
   "repository": {
     "type": "git",