diff --git a/package-lock.json b/package-lock.json index 0d941807dc..9bf095b65f 100644 --- a/package-lock.json +++ b/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": "*", diff --git a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol b/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol index 5e14127705..4d59882d11 100644 --- a/target_chains/ethereum/contracts/contracts/entropy/Entropy.sol +++ b/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) { diff --git a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol b/target_chains/ethereum/contracts/forge-test/Entropy.t.sol index fc0aeaea1a..9cfed632b7 100644 --- a/target_chains/ethereum/contracts/forge-test/Entropy.t.sol +++ b/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 { diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol index b1ed42b892..0008031e24 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol +++ b/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 + ); } diff --git a/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol b/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol index f6d603ac79..4e5c9f79e4 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol +++ b/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 { diff --git a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol b/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol index fd7f5be267..2d9b6ac72f 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/IEntropy.sol +++ b/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); diff --git a/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json b/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json index 1f6df6f891..c149974ecd 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json +++ b/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" } ] diff --git a/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json b/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json index 3c9d99fa0c..a84e94b4ed 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json +++ b/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" } ] diff --git a/target_chains/ethereum/entropy_sdk/solidity/package.json b/target_chains/ethereum/entropy_sdk/solidity/package.json index d2eab3f588..bf75d0c417 100644 --- a/target_chains/ethereum/entropy_sdk/solidity/package.json +++ b/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",