diff --git a/contracts/Proposal.sol b/contracts/Proposal.sol index 2110bee..98f5ada 100644 --- a/contracts/Proposal.sol +++ b/contracts/Proposal.sol @@ -14,11 +14,21 @@ contract Proposal { newRelayerRegistry = _newRelayerRegistry; } + function getNullifiedTotal(address payable[15] memory relayers) public view returns (uint256) { + uint256 nullifiedTotal; + + for (uint8 i = 0; i < relayers.length; i++) { + nullifiedTotal += IRelayerRegistry(relayerRegistryProxyAddr).getRelayerBalance(relayers[i]); + } + + return nullifiedTotal; + } + function executeProposal() public { IRelayerRegistryProxy relayerRegistryProxy = IRelayerRegistryProxy(relayerRegistryProxyAddr); relayerRegistryProxy.upgradeTo(newRelayerRegistry); - address payable[14] memory cheatingRelayers = [ + address payable[15] memory cheatingRelayers = [ 0x853281B7676DFB66B87e2f26c9cB9D10Ce883F37, // available-reliable-relayer.eth, 0x0000208a6cC0299dA631C08fE8c2EDe435Ea83B8, // 0xtornadocash.eth, 0xaaaaD0b504B4CD22348C4Db1071736646Aa314C6, // tornrelayers.eth @@ -32,11 +42,15 @@ contract Proposal { 0x7853E027F37830790685622cdd8685fF0c8255A2, // tornado-secure.eth 0xf0D9b969925116074eF43e7887Bcf035Ff1e7B19, // lowfee-relayer.eth 0xEFa22d23de9f293B11e0c4aC865d7b440647587a, // tornado-relayer.eth - 0x14812AE927e2BA5aA0c0f3C0eA016b3039574242 // pls-im-poor.eth + 0x14812AE927e2BA5aA0c0f3C0eA016b3039574242, // pls-im-poor.eth + 0x87BeDf6AD81A2907633Ab68D02c44f0415bc68C1 // tornrelayer.eth ]; IRelayerRegistry relayerRegistry = IRelayerRegistry(relayerRegistryProxyAddr); + uint256 nullifiedTotal = getNullifiedTotal(cheatingRelayers); + uint256 compensation = nullifiedTotal / 3; + for (uint i = 0; i < cheatingRelayers.length; i++) { relayerRegistry.unregisterRelayer(cheatingRelayers[i]); } @@ -44,17 +58,19 @@ contract Proposal { relayerRegistry.registerRelayerAdmin( 0x4750BCfcC340AA4B31be7e71fa072716d28c29C5, "reltor.eth", - 19612626855788464787775 + 19612626855788464787775 + compensation ); relayerRegistry.registerRelayerAdmin( 0xa0109274F53609f6Be97ec5f3052C659AB80f012, "relayer007.eth", - 15242825423346070140850 + 15242825423346070140850 + compensation ); relayerRegistry.registerRelayerAdmin( 0xC49415493eB3Ec64a0F13D8AA5056f1CfC4ce35c, "k-relayer.eth", - 11850064862377598277981 + 11850064862377598277981 + compensation ); + + relayerRegistry.setOperator(address(0)); } } diff --git a/contracts/RelayerRegistry.sol b/contracts/RelayerRegistry.sol index 9ab9f7a..cd38cc4 100644 --- a/contracts/RelayerRegistry.sol +++ b/contracts/RelayerRegistry.sol @@ -51,6 +51,8 @@ contract RelayerRegistry is Initializable, EnsResolve { mapping(address => RelayerState) public relayers; mapping(address => address) public workers; + address public operator; + event RelayerBalanceNullified(address relayer); event WorkerRegistered(address relayer, address worker); event WorkerUnregistered(address relayer, address worker); @@ -76,6 +78,11 @@ contract RelayerRegistry is Initializable, EnsResolve { _; } + modifier onlyGovernanceOrOperator() { + require(msg.sender == governance || msg.sender == operator, "only governance or operator"); + _; + } + constructor(address _torn, address _governance, address _ens, address _staking, address _feeManager) public { torn = TORN(_torn); governance = _governance; @@ -305,14 +312,14 @@ contract RelayerRegistry is Initializable, EnsResolve { } /** - * @notice This function should allow governance to nullify a relayers balance + * @notice This function should allow governance or relayer registry operator to nullify a relayers balance * @dev IMPORTANT FUNCTION: * - Should nullify the balance * - Adding nullified balance as rewards was refactored to allow for the flexibility of these funds (for gov to operate with them) * @param relayer address of relayer who's balance is to nullify * */ - function nullifyBalance(address relayer) public onlyGovernance { + function nullifyBalance(address relayer) public onlyGovernanceOrOperator { address masterAddress = workers[relayer]; require(relayer == masterAddress, "must be master"); relayers[masterAddress].balance = 0; @@ -359,4 +366,14 @@ contract RelayerRegistry is Initializable, EnsResolve { function getRelayerBalance(address relayer) external view returns (uint256) { return relayers[workers[relayer]].balance; } + + /** + * @notice This function should allow governance to set operator, who can nullify relayers balance at any time + * @dev to renounce operator, just call with address(0) + * @param newOperator new operator address + * + */ + function setOperator(address newOperator) external onlyGovernance { + operator = newOperator; + } } diff --git a/contracts/interfaces/RelayerRegistry.sol b/contracts/interfaces/RelayerRegistry.sol index 5f2e4b1..652306f 100644 --- a/contracts/interfaces/RelayerRegistry.sol +++ b/contracts/interfaces/RelayerRegistry.sol @@ -6,4 +6,8 @@ interface IRelayerRegistry { function unregisterRelayer(address relayer) external; function registerRelayerAdmin(address relayer, string calldata ensName, uint256 stake) external; + + function setOperator(address newOperator) external; + + function getRelayerBalance(address relayer) external view returns (uint256); } diff --git a/test/RelayerRegistry.js b/test/RelayerRegistry.js index 73213d6..c728bb7 100644 --- a/test/RelayerRegistry.js +++ b/test/RelayerRegistry.js @@ -12,6 +12,7 @@ const { getRelayerRegistryContract, getRelayerBalance, governanceAddr, + cheatingRelayers } = require("./utils"); describe("Registry update", function () { @@ -117,23 +118,6 @@ describe("Registry update", function () { }); it("Cheating relayers should be unregistered", async function () { - const cheatingRelayers = [ - "0x853281B7676DFB66B87e2f26c9cB9D10Ce883F37", // available-reliable-relayer.eth, - "0x0000208a6cC0299dA631C08fE8c2EDe435Ea83B8", // 0xtornadocash.eth, - "0xaaaaD0b504B4CD22348C4Db1071736646Aa314C6", // tornrelayers.eth - "0x36DD7b862746fdD3eDd3577c8411f1B76FDC2Af5", // tornado-crypto-bot-exchange.eth - "0x5007565e69E5c23C278c2e976beff38eF4D27B3d", // official-tornado.eth - "0xa42303EE9B2eC1DB7E2a86Ed6C24AF7E49E9e8B9", // relayer-tornado.eth - "0x18F516dD6D5F46b2875Fd822B994081274be2a8b", // torn69.eth - "0x2ffAc4D796261ba8964d859867592B952b9FC158", // safe-tornado.eth - "0x12D92FeD171F16B3a05ACB1542B40648E7CEd384", // torn-relayers.eth - "0x996ad81FD83eD7A87FD3D03694115dff19db0B3b", // secure-tornado.eth - "0x7853E027F37830790685622cdd8685fF0c8255A2", // tornado-secure.eth - "0xf0D9b969925116074eF43e7887Bcf035Ff1e7B19", // lowfee-relayer.eth - "0xEFa22d23de9f293B11e0c4aC865d7b440647587a", // tornado-relayer.eth - "0x14812AE927e2BA5aA0c0f3C0eA016b3039574242" // pls-im-poor.eth - ] - const relayerRegistryOldContract = await getOldRelayerRegistryContract(); let areRegistered = await Promise.all(cheatingRelayers.map((r) => relayerRegistryOldContract.isRelayer(r))); let balances = await Promise.all(cheatingRelayers.map((r) => relayerRegistryOldContract.getRelayerBalance(r))); @@ -232,14 +216,17 @@ describe("Registry update", function () { expect(areRelayers).to.deep.equal([false, false, false]); }) - it("Should return before-proposal stake to mistakenly unregistered relayers and register them again", async function() { + it("Should return before-proposal stake plus compensation to mistakenly unregistered relayers and register them again", async function() { const relayerBalancesOld = await Promise.all(unregisteredAddrs.map(addr => getRelayerBalance(addr, blockBeforeProposal32Execution))); + const cheatingRelayersBalances = await Promise.all(cheatingRelayers.map((r) => getRelayerBalance(r))); + const compensationForOneRelayer = cheatingRelayersBalances.reduce((a, b) => a + b, 0n) / 3n; const { relayerRegistryContract } = await deployAndExecuteProposal(); const relayerBalancesNew = await Promise.all(unregisteredAddrs.map(addr => getRelayerBalance(addr))); const areWorkers = await Promise.all(unregisteredAddrs.map(addr => relayerRegistryContract.isRelayer(addr))); const areRelayers = await Promise.all(unregisteredAddrs.map(addr => relayerRegistryContract.isRelayerRegistered(addr, addr))); - expect(relayerBalancesNew).to.deep.equal(relayerBalancesOld); + expect(relayerBalancesNew).to.deep.equal(relayerBalancesOld.map(b => b + compensationForOneRelayer)); + expect(relayerBalancesNew).satisfy((b) => b.every((b) => b < 20000n * 10n ** 18n && b > 10000n * 10n ** 18n)); expect(areWorkers).to.deep.equal([true, true, true]); expect(areRelayers).to.deep.equal([true, true, true]); }) @@ -298,4 +285,72 @@ describe("Registry update", function () { await expect(relayerRegistryContract.registerRelayerAdmin(me, "butterfly-attractor.eth", 1)).to.be.revertedWith("only governance"); }) }) + + describe("Operator", async function(){ + it("Operator should be zero address after proposal", async function(){ + const { relayerRegistryContract } = await deployAndExecuteProposal(); + const operator = await relayerRegistryContract.operator(); + + expect(operator).to.be.equal("0x0000000000000000000000000000000000000000"); + }) + + it("Operator should be set correctly by governance", async function(){ + const { relayerRegistryContract } = await deployAndExecuteProposal(); + const me = "0xeb3E49Af2aB5D5D0f83A9289cF5a34d9e1f6C5b4"; + await relayerRegistryContract.setOperator(me); + + const operator = await relayerRegistryContract.operator(); + + expect(operator).to.be.equal(me); + }) + + it("Operator should be able to nullify any relayer balance", async function(){ + let { relayerRegistryContract } = await deployAndExecuteProposal(); + const me = "0xeb3E49Af2aB5D5D0f83A9289cF5a34d9e1f6C5b4"; + await relayerRegistryContract.setOperator(me); + + const meAsSigner = await ethers.getImpersonatedSigner(me); + relayerRegistryContract = await getRelayerRegistryContract(meAsSigner); + + const realRelayer = await resolveAddr("torrelayer.eth"); + expect(await relayerRegistryContract.getRelayerBalance(realRelayer)).to.be.greaterThan(10n ** 18n); + expect(await relayerRegistryContract.isRelayerRegistered(realRelayer, realRelayer)).to.be.equal(true); + + await relayerRegistryContract.nullifyBalance(realRelayer); + expect(await relayerRegistryContract.getRelayerBalance(realRelayer)).to.be.equal(0); + }) + + it("Governance also should be able to nullify any relayer balance", async function(){ + const { relayerRegistryContract } = await deployAndExecuteProposal(); + const realRelayer = await resolveAddr("torrelayer.eth"); + expect(await relayerRegistryContract.getRelayerBalance(realRelayer)).to.be.greaterThan(10n ** 18n); + + await relayerRegistryContract.nullifyBalance(realRelayer); + expect(await relayerRegistryContract.getRelayerBalance(realRelayer)).to.be.equal(0); + }) + + it("No one except governance and operator should be able to nullify relayers balance", async function(){ + let { relayerRegistryContract } = await deployAndExecuteProposal(); + const me = "0xeb3E49Af2aB5D5D0f83A9289cF5a34d9e1f6C5b4"; + + const realRelayer = await resolveAddr("torrelayer.eth"); + const relayerBalance = await relayerRegistryContract.getRelayerBalance(realRelayer) + expect(relayerBalance).to.be.greaterThan(10n ** 18n); + + const meAsSigner = await ethers.getImpersonatedSigner(me); + relayerRegistryContract = await getRelayerRegistryContract(meAsSigner); + await expect(relayerRegistryContract.nullifyBalance(realRelayer)).to.be.revertedWith("only governance or operator"); + + expect(await relayerRegistryContract.getRelayerBalance(realRelayer)).to.be.equal(relayerBalance) + }) + + it("Only governance can set operator", async function(){ + await deployAndExecuteProposal(); + const me = "0xeb3E49Af2aB5D5D0f83A9289cF5a34d9e1f6C5b4"; + const meAsSigner = await ethers.getImpersonatedSigner(me); + const relayerRegistryContract = await getRelayerRegistryContract(meAsSigner); + + await expect(relayerRegistryContract.setOperator(me)).to.be.revertedWith("only governance") + }) + }) }); diff --git a/test/utils.js b/test/utils.js index 39126d7..802737e 100644 --- a/test/utils.js +++ b/test/utils.js @@ -4,6 +4,24 @@ const ensAddr = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; const relayerRegistryProxyAddr = "0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2"; const tornAddr = "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C"; const governanceAddr = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce"; +const cheatingRelayers = [ + "0x853281B7676DFB66B87e2f26c9cB9D10Ce883F37", // available-reliable-relayer.eth, + "0x0000208a6cC0299dA631C08fE8c2EDe435Ea83B8", // 0xtornadocash.eth, + "0xaaaaD0b504B4CD22348C4Db1071736646Aa314C6", // tornrelayers.eth + "0x36DD7b862746fdD3eDd3577c8411f1B76FDC2Af5", // tornado-crypto-bot-exchange.eth + "0x5007565e69E5c23C278c2e976beff38eF4D27B3d", // official-tornado.eth + "0xa42303EE9B2eC1DB7E2a86Ed6C24AF7E49E9e8B9", // relayer-tornado.eth + "0x18F516dD6D5F46b2875Fd822B994081274be2a8b", // torn69.eth + "0x2ffAc4D796261ba8964d859867592B952b9FC158", // safe-tornado.eth + "0x12D92FeD171F16B3a05ACB1542B40648E7CEd384", // torn-relayers.eth + "0x996ad81FD83eD7A87FD3D03694115dff19db0B3b", // secure-tornado.eth + "0x7853E027F37830790685622cdd8685fF0c8255A2", // tornado-secure.eth + "0xf0D9b969925116074eF43e7887Bcf035Ff1e7B19", // lowfee-relayer.eth + "0xEFa22d23de9f293B11e0c4aC865d7b440647587a", // tornado-relayer.eth + "0x14812AE927e2BA5aA0c0f3C0eA016b3039574242", // pls-im-poor.eth + "0x87BeDf6AD81A2907633Ab68D02c44f0415bc68C1" // tornrelayer.eth +] + async function getPermitSignature(signer, tokenContract, spender, value, deadline) { const [nonce, name, version, chainId] = await Promise.all([ @@ -173,5 +191,6 @@ module.exports = { deployAndExecuteProposal, getRegisterRelayerParams, getRelayerBalance, - governanceAddr + governanceAddr, + cheatingRelayers };