diff --git a/.changeset/bright-dodos-attack.md b/.changeset/bright-dodos-attack.md new file mode 100644 index 000000000..196b6fa60 --- /dev/null +++ b/.changeset/bright-dodos-attack.md @@ -0,0 +1,7 @@ +--- +'@chugsplash/contracts': minor +'@chugsplash/plugins': patch +'@chugsplash/core': patch +--- + +Add a protocol fee to be collected during execution diff --git a/packages/contracts/contracts/ChugSplashManager.sol b/packages/contracts/contracts/ChugSplashManager.sol index a2d03b411..5efef2913 100644 --- a/packages/contracts/contracts/ChugSplashManager.sol +++ b/packages/contracts/contracts/ChugSplashManager.sol @@ -145,6 +145,8 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { */ event ExecutorPaymentClaimed(address indexed executor, uint256 amount); + event ProtocolPaymentClaimed(address indexed recipient, uint256 amount); + /** * @notice Emitted when the owner withdraws ETH from this contract. * @@ -239,11 +241,13 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { */ uint256 public immutable executorPaymentPercentage; + uint256 public immutable protocolPaymentPercentage; + /** * @notice Mapping of executor addresses to the ETH amount stored in this contract that is * owed to them. */ - mapping(address => uint256) public debt; + mapping(address => uint256) public executorDebt; /** * @notice Maps an address to a boolean indicating if the address is allowed to propose bundles. @@ -270,7 +274,9 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { /** * @notice ETH amount that is owed to the executor. */ - uint256 public totalDebt; + uint256 public totalExecutorDebt; + + uint256 public totalProtocolDebt; /** * @notice Modifier that restricts access to the executor. @@ -298,13 +304,15 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { ChugSplashRecorder _recorder, uint256 _executionLockTime, uint256 _ownerBondAmount, - uint256 _executorPaymentPercentage + uint256 _executorPaymentPercentage, + uint256 _protocolPaymentPercentage ) { registry = _registry; recorder = _recorder; executionLockTime = _executionLockTime; ownerBondAmount = _ownerBondAmount; executorPaymentPercentage = _executorPaymentPercentage; + protocolPaymentPercentage = _protocolPaymentPercentage; } /** @@ -338,6 +346,10 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { keccak256(abi.encode(_actionRoot, _targetRoot, _numActions, _numTargets, _configUri)); } + function totalDebt() public view returns (uint256) { + return totalExecutorDebt + totalProtocolDebt; + } + /** * @notice Queries the selected executor for a given project/bundle. * @@ -442,7 +454,7 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { */ function approveChugSplashBundle(bytes32 _bundleId) public onlyOwner { require( - address(this).balance - totalDebt >= ownerBondAmount, + address(this).balance - totalDebt() >= ownerBondAmount, "ChugSplashManager: insufficient balance in manager" ); @@ -563,27 +575,32 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { emit ChugSplashBundleInitiated(activeBundleId, msg.sender); recorder.announce("ChugSplashBundleInitiated"); - // See the explanation in `executeChugSplashAction`. - uint256 gasUsed = 152778 + initialGasLeft - gasleft(); - - // Calculate the executor's payment. - uint256 executorPayment; + uint256 gasPrice; if (block.chainid != 10 && block.chainid != 420) { // Use the gas price for any network that isn't Optimism. - executorPayment = (tx.gasprice * gasUsed * (100 + executorPaymentPercentage)) / 100; + gasPrice = tx.gasprice; } else if (block.chainid == 10) { // Optimism mainnet does not include `tx.gasprice` in the transaction, so we hardcode // its value here. - executorPayment = (1000000 * gasUsed * (100 + executorPaymentPercentage)) / 100; + gasPrice = 1000000; } else { - // Optimism mainnet does not include `tx.gasprice` in the transaction, so we hardcode + // Optimism Goerli does not include `tx.gasprice` in the transaction, so we hardcode // its value here. - executorPayment = (gasUsed * (100 + executorPaymentPercentage)) / 100; + gasPrice = 1; } - // Add the executor's payment to the debt. - totalDebt += executorPayment; - debt[msg.sender] += executorPayment; + // See the explanation in `executeChugSplashAction`. + uint256 gasUsed = 152778 + initialGasLeft - gasleft(); + + uint256 executorPayment = (gasPrice * gasUsed * (100 + executorPaymentPercentage)) / 100; + uint256 protocolPayment = gasPrice * gasUsed * protocolPaymentPercentage; + + // Add the executor's payment to the executor debt. + totalExecutorDebt += executorPayment; + executorDebt[msg.sender] += executorPayment; + + // Add the protocol's payment to the protocol debt. + totalProtocolDebt += protocolPayment; } /** @@ -689,6 +706,20 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { emit ChugSplashActionExecuted(activeBundleId, _action.proxy, msg.sender, _actionIndex); recorder.announceWithData("ChugSplashActionExecuted", abi.encodePacked(_action.proxy)); + uint256 gasPrice; + if (block.chainid != 10 && block.chainid != 420) { + // Use the gas price for any network that isn't Optimism. + gasPrice = tx.gasprice; + } else if (block.chainid == 10) { + // Optimism mainnet does not include `tx.gasprice` in the transaction, so we hardcode + // its value here. + gasPrice = 1000000; + } else { + // Optimism Goerli does not include `tx.gasprice` in the transaction, so we hardcode + // its value here. + gasPrice = 1; + } + // Estimate the amount of gas used in this call by subtracting the current gas left from the // initial gas left. We add 152778 to this amount to account for the intrinsic gas cost // (21k), the calldata usage, and the subsequent opcodes that occur when we add the @@ -698,23 +729,15 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { // the side of safety by adding a larger value. TODO: Get a better estimate than 152778. uint256 gasUsed = 152778 + initialGasLeft - gasleft(); - // Calculate the executor's payment and add it to the debt owed to the executor. - uint256 executorPayment; - if (block.chainid != 10 && block.chainid != 420) { - // Use the gas price for any network that isn't Optimism. - executorPayment = (tx.gasprice * gasUsed * (100 + executorPaymentPercentage)) / 100; - } else if (block.chainid == 10) { - // Optimism mainnet does not include `tx.gasprice` in the transaction, so we hardcode - // its value here. - executorPayment = (1000000 * gasUsed * (100 + executorPaymentPercentage)) / 100; - } else { - // Optimism mainnet does not include `tx.gasprice` in the transaction, so we hardcode - // its value here. - executorPayment = (gasUsed * (100 + executorPaymentPercentage)) / 100; - } + uint256 executorPayment = (gasPrice * gasUsed * (100 + executorPaymentPercentage)) / 100; + uint256 protocolPayment = gasPrice * gasUsed * protocolPaymentPercentage; + + // Add the executor's payment to the executor debt. + totalExecutorDebt += executorPayment; + executorDebt[msg.sender] += executorPayment; - totalDebt += executorPayment; - debt[msg.sender] += executorPayment; + // Add the protocol's payment to the protocol debt. + totalProtocolDebt += protocolPayment; } /** @@ -803,27 +826,32 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { emit ChugSplashBundleCompleted(completedBundleId, msg.sender, bundle.actionsExecuted); recorder.announce("ChugSplashBundleCompleted"); - // See the explanation in `executeChugSplashAction`. - uint256 gasUsed = 152778 + initialGasLeft - gasleft(); - - // Calculate the executor's payment. - uint256 executorPayment; + uint256 gasPrice; if (block.chainid != 10 && block.chainid != 420) { // Use the gas price for any network that isn't Optimism. - executorPayment = (tx.gasprice * gasUsed * (100 + executorPaymentPercentage)) / 100; + gasPrice = tx.gasprice; } else if (block.chainid == 10) { // Optimism mainnet does not include `tx.gasprice` in the transaction, so we hardcode // its value here. - executorPayment = (1000000 * gasUsed * (100 + executorPaymentPercentage)) / 100; + gasPrice = 1000000; } else { - // Optimism mainnet does not include `tx.gasprice` in the transaction, so we hardcode + // Optimism Goerli does not include `tx.gasprice` in the transaction, so we hardcode // its value here. - executorPayment = (gasUsed * (100 + executorPaymentPercentage)) / 100; + gasPrice = 1; } - // Add the executor's payment to the debt. - totalDebt += executorPayment; - debt[msg.sender] += executorPayment; + // See the explanation in `executeChugSplashAction`. + uint256 gasUsed = 152778 + initialGasLeft - gasleft(); + + uint256 executorPayment = (gasPrice * gasUsed * (100 + executorPaymentPercentage)) / 100; + uint256 protocolPayment = gasPrice * gasUsed * protocolPaymentPercentage; + + // Add the executor's payment to the executor debt. + totalExecutorDebt += executorPayment; + executorDebt[msg.sender] += executorPayment; + + // Add the protocol's payment to the protocol debt. + totalProtocolDebt += protocolPayment; } /** @@ -844,7 +872,7 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { if (bundle.timeClaimed + executionLockTime >= block.timestamp) { // Give the owner's bond to the executor if the bundle is cancelled within the // `executionLockTime` window. - totalDebt += ownerBondAmount; + totalExecutorDebt += ownerBondAmount; } bytes32 cancelledBundleId = activeBundleId; @@ -885,18 +913,34 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { * ETH that is owed to them by this contract. */ function claimExecutorPayment() external onlyExecutor { - uint256 amount = debt[msg.sender]; + uint256 amount = executorDebt[msg.sender]; - debt[msg.sender] -= amount; - totalDebt -= amount; + executorDebt[msg.sender] -= amount; + totalExecutorDebt -= amount; (bool success, ) = payable(msg.sender).call{ value: amount }(new bytes(0)); - require(success, "ChugSplashManager: call to withdraw owner funds failed"); + require(success, "ChugSplashManager: call to withdraw executor funds failed"); emit ExecutorPaymentClaimed(msg.sender, amount); recorder.announce("ExecutorPaymentClaimed"); } + function claimProtocolPayment() external { + require( + registry.protocolPaymentRecipients(msg.sender) == true, + "ChugSplashManager: caller is not a protocol payment recipient" + ); + + uint256 amount = totalProtocolDebt; + totalProtocolDebt = 0; + + (bool success, ) = payable(msg.sender).call{ value: amount }(new bytes(0)); + require(success, "ChugSplashManager: call to withdraw protocol funds failed"); + + emit ProtocolPaymentClaimed(msg.sender, amount); + recorder.announce("ProtocolPaymentClaimed"); + } + /** * @notice Transfers ownership of a proxy from this contract to the project owner. * @@ -934,7 +978,7 @@ contract ChugSplashManager is OwnableUpgradeable, ReentrancyGuardUpgradeable { "ChugSplashManager: cannot withdraw funds while bundle is active" ); - uint256 amount = address(this).balance - totalDebt; + uint256 amount = address(this).balance - totalDebt(); (bool success, ) = payable(msg.sender).call{ value: amount }(new bytes(0)); require(success, "ChugSplashManager: call to withdraw owner funds failed"); diff --git a/packages/contracts/contracts/ChugSplashRegistry.sol b/packages/contracts/contracts/ChugSplashRegistry.sol index 2f23153cc..11271d4b1 100644 --- a/packages/contracts/contracts/ChugSplashRegistry.sol +++ b/packages/contracts/contracts/ChugSplashRegistry.sol @@ -60,6 +60,10 @@ contract ChugSplashRegistry is Initializable, OwnableUpgradeable { */ event ExecutorRemoved(address indexed executor); + event ProtocolPaymentRecipientAdded(address indexed executor); + + event ProtocolPaymentRecipientRemoved(address indexed executor); + /** * @notice Mapping of project names to ChugSplashManager contracts. */ @@ -70,6 +74,8 @@ contract ChugSplashRegistry is Initializable, OwnableUpgradeable { */ mapping(address => bool) public executors; + mapping(address => bool) public protocolPaymentRecipients; + ChugSplashRecorder public recorder; /** @@ -189,6 +195,24 @@ contract ChugSplashRegistry is Initializable, OwnableUpgradeable { emit ExecutorRemoved(_executor); } + function addProtocolPaymentRecipient(address _recipient) external onlyOwner { + require( + protocolPaymentRecipients[_recipient] == false, + "ChugSplashRegistry: recipient already added" + ); + protocolPaymentRecipients[_recipient] = true; + emit ProtocolPaymentRecipientAdded(_recipient); + } + + function removeProtocolPaymentRecipient(address _recipient) external onlyOwner { + require( + protocolPaymentRecipients[_recipient] == true, + "ChugSplashRegistry: recipient already removed" + ); + protocolPaymentRecipients[_recipient] = false; + emit ProtocolPaymentRecipientRemoved(_recipient); + } + /** * @notice Internal function that gets the ChugSplashManager implementation address. Will only * return a valid value when this contract is delegatecalled by the diff --git a/packages/contracts/src/constants.ts b/packages/contracts/src/constants.ts index 80753260f..0df06be18 100644 --- a/packages/contracts/src/constants.ts +++ b/packages/contracts/src/constants.ts @@ -63,6 +63,7 @@ export const DETERMINISTIC_DEPLOYMENT_PROXY_ADDRESS = export const OWNER_BOND_AMOUNT = ethers.utils.parseEther('0.001') export const EXECUTION_LOCK_TIME = 15 * 60 export const EXECUTOR_PAYMENT_PERCENTAGE = 20 +export const PROTOCOL_PAYMENT_PERCENTAGE = 20 export const CHUGSPLASH_BOOTLOADER_ADDRESS = ethers.utils.getCreate2Address( DETERMINISTIC_DEPLOYMENT_PROXY_ADDRESS, diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 2517580c1..c88c2c84a 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -18,6 +18,7 @@ import { DEFAULT_UPDATER_ADDRESS, registryProxyConstructorArgValues, proxyInitializerConstructorArgValues, + PROTOCOL_PAYMENT_PERCENTAGE, ChugSplashManagerABI, DETERMINISTIC_DEPLOYMENT_PROXY_ADDRESS, CHUGSPLASH_SALT, @@ -59,6 +60,7 @@ export const chugsplashManagerConstructorArgs = { _executionLockTime: EXECUTION_LOCK_TIME, _ownerBondAmount: OWNER_BOND_AMOUNT.toString(), _executorPaymentPercentage: EXECUTOR_PAYMENT_PERCENTAGE, + _protocolPaymentPercentage: PROTOCOL_PAYMENT_PERCENTAGE, } export const CHUGSPLASH_CONSTRUCTOR_ARGS = {} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index dafa7d296..882e36a26 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -358,7 +358,7 @@ export const claimExecutorPayment = async ( executor: Wallet, ChugSplashManager: Contract ) => { - const executorDebt = await ChugSplashManager.debt(executor.address) + const executorDebt = await ChugSplashManager.executorDebt(executor.address) if (executorDebt.gt(0)) { await ( await ChugSplashManager.claimExecutorPayment( diff --git a/packages/plugins/chugsplash/hardhat/UUPSAccessControlUpgradableUpgrade.config.ts b/packages/plugins/chugsplash/hardhat/UUPSAccessControlUpgradableUpgrade.config.ts index 9026c68ce..2a915f0ed 100644 --- a/packages/plugins/chugsplash/hardhat/UUPSAccessControlUpgradableUpgrade.config.ts +++ b/packages/plugins/chugsplash/hardhat/UUPSAccessControlUpgradableUpgrade.config.ts @@ -22,7 +22,7 @@ const config: UserChugSplashConfig = { 'UUPSUpgradeable:__gap': [], _roles: [], }, - externalProxy: '0x62DB6c1678Ca81ea0d946EA3dd75b4F71421A2aE', + externalProxy: '0x9A7848b9E60C7619f162880c7CA5Cbca80998034', externalProxyType: 'oz-access-control-uups', // We must specify these explicitly because newer versions of OpenZeppelin's Hardhat plugin // don't create the Network file in the `.openzeppelin/` folder anymore: diff --git a/packages/plugins/chugsplash/hardhat/UUPSOwnableUpgradableUpgrade.config.ts b/packages/plugins/chugsplash/hardhat/UUPSOwnableUpgradableUpgrade.config.ts index 4db839dc0..ead94b48d 100644 --- a/packages/plugins/chugsplash/hardhat/UUPSOwnableUpgradableUpgrade.config.ts +++ b/packages/plugins/chugsplash/hardhat/UUPSOwnableUpgradableUpgrade.config.ts @@ -21,7 +21,7 @@ const config: UserChugSplashConfig = { 'UUPSUpgradeable:__gap': [], _owner: '{ preserve }', }, - externalProxy: '0xA7c8B0D74b68EF10511F27e97c379FB1651e1eD2', + externalProxy: '0xE9061F92bA9A3D9ef3f4eb8456ac9E552B3Ff5C8', externalProxyType: 'oz-ownable-uups', // We must specify these explicitly because newer versions of OpenZeppelin's Hardhat plugin // don't create the Network file in the `.openzeppelin/` folder anymore: