From 6a47e18b355fb3089407d1fed312a34eb14b435b Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Mon, 23 Mar 2026 14:11:43 +0300 Subject: [PATCH] avoid DOS for validators; limit bridging to existential deposit Signed-off-by: Uncle Fatso --- .env.template | 2 ++ src/Gatekeeper.sol | 4 ++++ src/interfaces/IGatekeeper.sol | 2 ++ test/gatekeeper/Gatekeeper.t.sol | 23 +++++++++++++++++++---- test/gatekeeper/GatekeeperMetadata.t.sol | 3 ++- test/staking/Staking.t.sol | 2 +- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.env.template b/.env.template index c64e965..9d2fbda 100644 --- a/.env.template +++ b/.env.template @@ -106,11 +106,13 @@ GOVERNOR_QUORUM_FRACTION= ###################### Initial ghosted supply on gatekeeper ########################## ## ghostedSupply - supply that is currently locked inside the gatekeeper ## +## existential - minimum amount that could be reflected inside other chain ## ## ghostedIn - historical supply that was bridged in (max value is 2^104) ## ## ghostedOut - historical supply that was bridged out (max value is 2^104) ## ## deployedAt - time when gatekeepers was deployed, could be inherited if needed ## ###################################################################################### INITIAL_GHOSTED_SUPPLY= +INITIAL_EXISTENTIAL_DEPOSIT= INITIAL_GHOSTED_IN= INITIAL_GHOSTED_OUT= GATEKEEPER_DEPLOYED_AT= diff --git a/src/Gatekeeper.sol b/src/Gatekeeper.sol index cec5e36..b7ef01c 100644 --- a/src/Gatekeeper.sol +++ b/src/Gatekeeper.sol @@ -7,6 +7,7 @@ import {IGatekeeperMetadata} from "./interfaces/IGatekeeperMetadata.sol"; contract Gatekeeper is IGatekeeper, IGatekeeperMetadata { uint256 public constant DIVISOR = 1e6; + uint256 public override existentialDeposit; uint256 public override ghostedSupply; address public immutable staking; // forge-lint: disable-line(screaming-snake-case-immutable) @@ -17,10 +18,12 @@ contract Gatekeeper is IGatekeeper, IGatekeeperMetadata { constructor( address _staking, uint256 _ghostedSupply, + uint256 _existential, uint48 _deployedAt, uint104 _amountIn, uint104 _amountOut ) { + existentialDeposit = _existential; ghostedSupply = _ghostedSupply; staking = _staking; _metadata = GatekeeperMetadata({ @@ -36,6 +39,7 @@ contract Gatekeeper is IGatekeeper, IGatekeeperMetadata { function ghost(bytes32 receiver, uint256 amount) external override { if (msg.sender != staking) revert NotStaking(); + if (amount < existentialDeposit) revert NonExistentAmount(); ghostedSupply += amount; _metadata.amountIn += uint104(amount); // forge-lint: disable-line(unsafe-typecast) emit Ghosted(receiver, amount); diff --git a/src/interfaces/IGatekeeper.sol b/src/interfaces/IGatekeeper.sol index 9b93bdf..fdd2a80 100644 --- a/src/interfaces/IGatekeeper.sol +++ b/src/interfaces/IGatekeeper.sol @@ -5,12 +5,14 @@ interface IGatekeeper { error NotStaking(); error WrongSignature(); error AlreadyExecuted(); + error NonExistentAmount(); event Ghosted(bytes32 indexed receiver, uint256 indexed amount); event Materialized(address indexed receiver, uint256 indexed amount); event Rotated(uint256 indexed aggregatedPublicKey); function ghostedSupply() external view returns (uint256); + function existentialDeposit() external view returns (uint256); function ghost(bytes32 receiver, uint256 amount) external; function materialize(address receiver, uint256 amount, uint256 rx, uint256 s) external; function rotate(uint256 aggregatedPublicKey, uint256 rx, uint256 s) external; diff --git a/test/gatekeeper/Gatekeeper.t.sol b/test/gatekeeper/Gatekeeper.t.sol index 4e4cf55..cecc2c1 100644 --- a/test/gatekeeper/Gatekeeper.t.sol +++ b/test/gatekeeper/Gatekeeper.t.sol @@ -7,26 +7,29 @@ import {Gatekeeper} from "../../src/Gatekeeper.sol"; contract GatekeeperTest is Test { address constant ALICE = 0x0000000000000000000000000000000000000001; address constant BOB = 0x0000000000000000000000000000000000000002; + uint256 constant EXISTENTIAL = 1337; uint256 constant INIT_AMOUNT = 69 * 1e18; Gatekeeper gatekeeper; event Ghosted(bytes32 indexed receiver, uint256 indexed amount); function setUp() public { - gatekeeper = new Gatekeeper(ALICE, 0, 0, 0, 0); + gatekeeper = new Gatekeeper(ALICE, 0, EXISTENTIAL, 0, 0, 0); } function test_correctInitialization() public { assertEq(gatekeeper.staking(), ALICE); assertEq(gatekeeper.ghostedSupply(), 0); - Gatekeeper anotherGatekeeper = new Gatekeeper(BOB, INIT_AMOUNT, 0, 0, 0); + Gatekeeper anotherGatekeeper = new Gatekeeper(BOB, INIT_AMOUNT, EXISTENTIAL, 0, 0, 0); assertEq(anotherGatekeeper.staking(), BOB); assertEq(anotherGatekeeper.ghostedSupply(), INIT_AMOUNT); + assertEq(anotherGatekeeper.existentialDeposit(), EXISTENTIAL); + assertEq(anotherGatekeeper.ghostedSupply(), INIT_AMOUNT); } function test_ghostTokensWork(uint256 ghostAmount) public { - vm.assume(ghostAmount > 0); + vm.assume(ghostAmount >= EXISTENTIAL); bytes32 receiver = bytes32(abi.encodePacked(ALICE)); uint256 ghostedSupply = gatekeeper.ghostedSupply(); @@ -46,7 +49,7 @@ contract GatekeeperTest is Test { } function test_ghostTokensEmitsEvent(uint256 ghostAmount) public { - vm.assume(ghostAmount > 0); + vm.assume(ghostAmount >= EXISTENTIAL); bytes32 receiver = bytes32(abi.encodePacked(ALICE)); vm.expectEmit(true, true, true, false, address(gatekeeper)); @@ -64,4 +67,16 @@ contract GatekeeperTest is Test { vm.expectRevert(); gatekeeper.rotate(aggregatedPublicKey, 0, 0); } + + function test_couldNotBridgeBelowExistential(uint256 amount) public { + vm.assume(amount < EXISTENTIAL); + bytes32 receiver = bytes32(abi.encodePacked(ALICE)); + + vm.expectRevert(); + vm.prank(ALICE); + gatekeeper.ghost(receiver, amount); + + assertEq(gatekeeper.ghostedSupply(), 0); + + } } diff --git a/test/gatekeeper/GatekeeperMetadata.t.sol b/test/gatekeeper/GatekeeperMetadata.t.sol index 9719b4b..8875978 100644 --- a/test/gatekeeper/GatekeeperMetadata.t.sol +++ b/test/gatekeeper/GatekeeperMetadata.t.sol @@ -8,6 +8,7 @@ contract GatekeeperMetadataTest is Test { address constant ALICE = 0x0000000000000000000000000000000000000001; uint256 constant INIT_AMOUNT = 69 * 1e18; uint256 constant INIT_GHOSTED = type(uint104).max / 2; + uint256 constant EXISTENTIAL = 0; uint48 constant DEPLOYED_AT = 1337; uint104 constant AMOUNT_IN = 69; uint104 constant AMOUNT_OUT = 420; @@ -15,7 +16,7 @@ contract GatekeeperMetadataTest is Test { Gatekeeper gatekeeper; function setUp() public { - gatekeeper = new Gatekeeper(ALICE, INIT_GHOSTED, DEPLOYED_AT, AMOUNT_IN, AMOUNT_OUT); + gatekeeper = new Gatekeeper(ALICE, INIT_GHOSTED, EXISTENTIAL, DEPLOYED_AT, AMOUNT_IN, AMOUNT_OUT); } function test_correctMetadataInitialization() public view { diff --git a/test/staking/Staking.t.sol b/test/staking/Staking.t.sol index 95b432b..1cc3010 100644 --- a/test/staking/Staking.t.sol +++ b/test/staking/Staking.t.sol @@ -73,7 +73,7 @@ contract StakingTest is Test { treasury = new GhostTreasury(address(ftso), 69, address(authority)); stnk.initialize(address(staking), address(treasury), address(ghst)); ghst.initialize(address(staking)); - gatekeeper = new Gatekeeper(address(staking), 0, 0, 0, 0); + gatekeeper = new Gatekeeper(address(staking), 0, 0, 0, 0, 0); calculator = new GhostBondingCalculator(address(ftso), 1, 1); vm.stopPrank(); }