diff --git a/contracts/CommonErrors.sol b/contracts/CommonErrors.sol index cedccd39..88419e0a 100644 --- a/contracts/CommonErrors.sol +++ b/contracts/CommonErrors.sol @@ -27,3 +27,4 @@ error GroupIndexIsInvalid(uint256 index); error IsNotContract(address account); error NotEnoughFunds(); error RoleRequired(bytes32 role); +error TokensTransferFailure(); diff --git a/contracts/delegation/Distributor.sol b/contracts/delegation/Distributor.sol index 8b3ab460..af21d9ae 100644 --- a/contracts/delegation/Distributor.sol +++ b/contracts/delegation/Distributor.sol @@ -19,24 +19,21 @@ along with SKALE Manager. If not, see . */ -pragma solidity 0.8.17; - -import {IERC1820Registry} from "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol"; -import {IERC777Recipient} from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IDistributor} from "@skalenetwork/skale-manager-interfaces/delegation/IDistributor.sol"; -import { - IValidatorService -} from "@skalenetwork/skale-manager-interfaces/delegation/IValidatorService.sol"; -import { - IDelegationController -} from "@skalenetwork/skale-manager-interfaces/delegation/IDelegationController.sol"; -import {ITimeHelpers} from "@skalenetwork/skale-manager-interfaces/delegation/ITimeHelpers.sol"; - -import {Permissions} from "../Permissions.sol"; -import {ConstantsHolder} from "../ConstantsHolder.sol"; -import {MathUtils} from "../utils/MathUtils.sol"; +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC777Recipient } from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; +import { IERC1820Registry } from "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol"; +import { IDelegationController } +from "@skalenetwork/skale-manager-interfaces/delegation/IDelegationController.sol"; +import { IDistributor } from "@skalenetwork/skale-manager-interfaces/delegation/IDistributor.sol"; +import { ITimeHelpers } from "@skalenetwork/skale-manager-interfaces/delegation/ITimeHelpers.sol"; +import { IValidatorService } +from "@skalenetwork/skale-manager-interfaces/delegation/IValidatorService.sol"; +import { IConstantsHolder } from "@skalenetwork/skale-manager-interfaces/IConstantsHolder.sol"; +import { TokensTransferFailure } from "./../CommonErrors.sol"; +import { Permissions } from "./../Permissions.sol"; +import { MathUtils } from "./../utils/MathUtils.sol"; /** * @title Distributor @@ -48,15 +45,17 @@ contract Distributor is Permissions, IERC777Recipient, IDistributor { IERC1820Registry private _erc1820; - // validatorId => month => token - mapping(uint256 => mapping(uint256 => uint256)) private _bountyPaid; - // validatorId => month => token - mapping(uint256 => mapping(uint256 => uint256)) private _feePaid; - // holder => validatorId => month - mapping(address => mapping(uint256 => uint256)) - private _firstUnwithdrawnMonth; - // validatorId => month - mapping(uint256 => uint256) private _firstUnwithdrawnMonthForValidator; + mapping(uint256 validatorId => mapping(uint256 month => uint256 amount)) private _bountyPaid; + mapping(uint256 validatorId => mapping(uint256 month => uint256 amount)) private _feePaid; + mapping( + address holder => mapping(uint256 validatorId => uint256 month) + ) private _firstUnwithdrawnMonth; + mapping(uint256 validatorId => uint256 month) private _firstUnwithdrawnMonthForValidator; + + error BountyIsLocked(); + error DataLengthIsIncorrect(); + error FeeIsLocked(); + error ReceiverIsIncorrect(); function initialize(address contractsAddress) public override initializer { Permissions.initialize(contractsAddress); @@ -91,18 +90,16 @@ contract Distributor is Permissions, IERC777Recipient, IDistributor { ITimeHelpers timeHelpers = ITimeHelpers( contractManager.getContract("TimeHelpers") ); - ConstantsHolder constantsHolder = ConstantsHolder( + IConstantsHolder constantsHolder = IConstantsHolder( contractManager.getContract("ConstantsHolder") ); - require( - block.timestamp >= - timeHelpers.addMonths( + if (block.timestamp < timeHelpers.addMonths( constantsHolder.launchTimestamp(), constantsHolder.BOUNTY_LOCKUP_MONTHS() - ), - "Bounty is locked" - ); + )) { + revert BountyIsLocked(); + } uint256 bounty; uint256 endMonth; @@ -114,7 +111,9 @@ contract Distributor is Permissions, IERC777Recipient, IDistributor { _firstUnwithdrawnMonth[msg.sender][validatorId] = endMonth; IERC20 skaleToken = IERC20(contractManager.getContract("SkaleToken")); - require(skaleToken.transfer(to, bounty), "Failed to transfer tokens"); + if(!skaleToken.transfer(to, bounty)) { + revert TokensTransferFailure(); + } emit WithdrawBounty(msg.sender, validatorId, to, bounty); } @@ -137,18 +136,17 @@ contract Distributor is Permissions, IERC777Recipient, IDistributor { ITimeHelpers timeHelpers = ITimeHelpers( contractManager.getContract("TimeHelpers") ); - ConstantsHolder constantsHolder = ConstantsHolder( + IConstantsHolder constantsHolder = IConstantsHolder( contractManager.getContract("ConstantsHolder") ); - require( - block.timestamp >= - timeHelpers.addMonths( - constantsHolder.launchTimestamp(), - constantsHolder.BOUNTY_LOCKUP_MONTHS() - ), - "Fee is locked" - ); + if (block.timestamp < timeHelpers.addMonths( + constantsHolder.launchTimestamp(), + constantsHolder.BOUNTY_LOCKUP_MONTHS() + )) { + revert FeeIsLocked(); + } + // check Validator Exist inside getValidatorId uint256 validatorId = validatorService.getValidatorId(msg.sender); @@ -158,7 +156,9 @@ contract Distributor is Permissions, IERC777Recipient, IDistributor { _firstUnwithdrawnMonthForValidator[validatorId] = endMonth; - require(skaleToken.transfer(to, fee), "Failed to transfer tokens"); + if(!skaleToken.transfer(to, fee)) { + revert TokensTransferFailure(); + } emit WithdrawFee(validatorId, to, fee); } @@ -171,8 +171,12 @@ contract Distributor is Permissions, IERC777Recipient, IDistributor { bytes calldata userData, bytes calldata ) external override allow("SkaleToken") { - require(to == address(this), "Receiver is incorrect"); - require(userData.length == 32, "Data length is incorrect"); + if (to != address(this)) { + revert ReceiverIsIncorrect(); + } + if (userData.length != 32) { + revert DataLengthIsIncorrect(); + } uint256 validatorId = abi.decode(userData, (uint256)); _distributeBounty(amount, validatorId); } diff --git a/contracts/test/SkaleTokenInternalTester.sol b/contracts/test/SkaleTokenInternalTester.sol index 0cfa6c19..998edb47 100644 --- a/contracts/test/SkaleTokenInternalTester.sol +++ b/contracts/test/SkaleTokenInternalTester.sol @@ -21,6 +21,7 @@ pragma solidity 0.8.17; +import { IERC777Recipient } from "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol"; import { SkaleToken } from "../SkaleToken.sol"; import { ISkaleTokenInterfaceTester } from "./interfaces/ISkaleTokenInterfaceTester.sol"; @@ -32,6 +33,30 @@ contract SkaleTokenInternalTester is SkaleToken, ISkaleTokenInterfaceTester { // solhint-disable-next-line no-empty-blocks { } + function callTokensReceived( + address implementer, + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) + external + override + { + if (implementer != address(0)) { + IERC777Recipient(implementer).tokensReceived({ + operator: operator, + from: from, + to: to, + amount: amount, + userData: userData, + operatorData: operatorData + }); + } + } + function getMsgData() external view override returns (bytes memory msgData) { return _msgData(); } diff --git a/contracts/test/interfaces/ISkaleTokenInterfaceTester.sol b/contracts/test/interfaces/ISkaleTokenInterfaceTester.sol index 5bdc2150..df5104b6 100644 --- a/contracts/test/interfaces/ISkaleTokenInterfaceTester.sol +++ b/contracts/test/interfaces/ISkaleTokenInterfaceTester.sol @@ -23,5 +23,15 @@ pragma solidity 0.8.17; interface ISkaleTokenInterfaceTester { + function callTokensReceived( + address implementer, + address operator, + address from, + address to, + uint256 amount, + bytes memory userData, + bytes memory operatorData + ) + external; function getMsgData() external view returns (bytes memory msgData); } diff --git a/contracts/utils/MathUtils.sol b/contracts/utils/MathUtils.sol index 08225e8a..fe07bf61 100644 --- a/contracts/utils/MathUtils.sol +++ b/contracts/utils/MathUtils.sol @@ -19,7 +19,7 @@ along with SKALE Manager. If not, see . */ -pragma solidity 0.8.17; +pragma solidity ^0.8.17; library MathUtils { diff --git a/package.json b/package.json index 0d3fa039..6287ae9d 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@skalenetwork/ima-interfaces": "2.0.0-develop.67", "@skalenetwork/marionette-interfaces": "^0.0.0-main.6", "@skalenetwork/paymaster-interfaces": "^1.0.0-main.10", - "@skalenetwork/skale-manager-interfaces": "3.2.0-develop.0", + "@skalenetwork/skale-manager-interfaces": "3.2.0-develop.1", "@skalenetwork/upgrade-tools": "^3.0.0-linter.42", "@typechain/hardhat": "^9.1.0", "dotenv": "^16.3.1", diff --git a/test/delegation/Delegation.ts b/test/delegation/Delegation.ts index ea207a74..2018ab1c 100644 --- a/test/delegation/Delegation.ts +++ b/test/delegation/Delegation.ts @@ -234,6 +234,30 @@ describe("Delegation", () => { .should.be.eventually.rejectedWith("Limit of validators is reached"); }); + it("should revert on incorrect transfers", async () => { + const skaleTokenInternalTesterFactory = await ethers.getContractFactory("SkaleTokenInternalTester"); + const skaleTokenInternalTester = await skaleTokenInternalTesterFactory.deploy(contractManager, []); + await contractManager.setContractsAddress("SkaleToken", skaleTokenInternalTester); + await skaleTokenInternalTester.callTokensReceived( + distributor, + ethers.ZeroAddress, + holder1, + holder2, // Receiver always needs to be the Distributor + 5, + "0x", + "0x" + ).should.be.revertedWithCustomError(distributor, "ReceiverIsIncorrect"); + await skaleTokenInternalTester.callTokensReceived( + distributor, + ethers.ZeroAddress, + holder1, + distributor, + 5, + "0x", // Data length always must be 32 bytes + "0x" + ).should.be.revertedWithCustomError(distributor, "DataLengthIsIncorrect"); + }) + describe("when holders have tokens and validator is registered", () => { let validatorId: bigint; fastBeforeEach(async () => { @@ -599,9 +623,9 @@ describe("Delegation", () => { validatorId))[0].should.be.equal(31); await distributor.connect(validator).withdrawFee(bountyAddress.address) - .should.be.eventually.rejectedWith("Fee is locked"); + .should.be.revertedWithCustomError(distributor, "FeeIsLocked"); await distributor.connect(holder1).withdrawBounty(validatorId, bountyAddress.address) - .should.be.eventually.rejectedWith("Bounty is locked"); + .should.be.revertedWithCustomError(distributor, "BountyIsLocked"); await nextMonth(contractManager, 3); diff --git a/yarn.lock b/yarn.lock index 8e1f9481..6db439d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1741,10 +1741,10 @@ dependencies: axios "^1.4.0" -"@skalenetwork/skale-manager-interfaces@3.2.0-develop.0": - version "3.2.0-develop.0" - resolved "https://registry.yarnpkg.com/@skalenetwork/skale-manager-interfaces/-/skale-manager-interfaces-3.2.0-develop.0.tgz#01e64eeabf2b14b320074eb588b557bbd33bab39" - integrity sha512-f++aLtSwjLJgAb3dHAm/SMHvx2a0L3uWwF7aam1wCtB6YZrFLR3flzlH3Ms3JlLZtfl5iE7VBmsuidhAqpbfpw== +"@skalenetwork/skale-manager-interfaces@3.2.0-develop.1": + version "3.2.0-develop.1" + resolved "https://registry.yarnpkg.com/@skalenetwork/skale-manager-interfaces/-/skale-manager-interfaces-3.2.0-develop.1.tgz#ec4ad8f2b4d12a5ff8d03763674a214be61c773d" + integrity sha512-ACugcMzRFa7nbHcJ8mZE/J60M8GW9gJKsqmQYEYoElzZBMTCstEeWy1ff6Vj7kcv+xESHPPhTzJ34ieMCL3pFg== "@skalenetwork/skale-manager-interfaces@^3.2.0-develop.0": version "3.2.0-paymaster.2"