diff --git a/ops/deploy_swarm_contracts_zksync.sh b/ops/deploy_swarm_contracts_zksync.sh index a71f784..a808448 100755 --- a/ops/deploy_swarm_contracts_zksync.sh +++ b/ops/deploy_swarm_contracts_zksync.sh @@ -54,6 +54,7 @@ # - COUNTRY_MULTIPLIER: (optional) Country multiplier for bond calculation (0 = use default) # - BOND_QUOTA: (optional) Max bond amount sponsorable per period in wei # - BOND_PERIOD: (optional) Quota renewal period in seconds +# - FLEET_OPERATOR_BOND_ALLOWANCE: (optional) Initial bond allowance for fleet operator in wei (defaults to BOND_QUOTA) # # ============================================================================= @@ -190,6 +191,7 @@ preflight_checks() { export PAYMASTER_WITHDRAWER="${PAYMASTER_WITHDRAWER:-$L2_ADMIN}" export BOND_QUOTA="${BOND_QUOTA:-100000000000000000000000}" # 100000 NODL default export BOND_PERIOD="${BOND_PERIOD:-86400}" # 1 day default + export FLEET_OPERATOR_BOND_ALLOWANCE="${FLEET_OPERATOR_BOND_ALLOWANCE:-$BOND_QUOTA}" log_success "Pre-flight checks passed" } @@ -347,6 +349,7 @@ deploy_contracts() { log_info " FLEET_OPERATOR: $FLEET_OPERATOR" log_info " BOND_QUOTA: $BOND_QUOTA" log_info " BOND_PERIOD: $BOND_PERIOD" + log_info " FLEET_OPERATOR_BOND_ALLOWANCE: $FLEET_OPERATOR_BOND_ALLOWANCE" log_info " RPC: $RPC_URL" return 0 fi @@ -616,6 +619,7 @@ print_summary() { echo " Base Bond: $BASE_BOND wei" echo " Bond Quota: $BOND_QUOTA wei" echo " Bond Period: $BOND_PERIOD seconds" + echo " Operator Allowance: $FLEET_OPERATOR_BOND_ALLOWANCE wei" echo "" echo "==============================================" } diff --git a/script/DeploySwarmUpgradeableZkSync.s.sol b/script/DeploySwarmUpgradeableZkSync.s.sol index 882d0b2..305de6c 100644 --- a/script/DeploySwarmUpgradeableZkSync.s.sol +++ b/script/DeploySwarmUpgradeableZkSync.s.sol @@ -28,6 +28,7 @@ import {BondTreasuryPaymaster} from "../src/paymasters/BondTreasuryPaymaster.sol * - BOND_QUOTA: (optional) Max bond amount sponsorable per period in wei (defaults to 100k NODL) * - BOND_PERIOD: (optional) Quota renewal period in seconds (defaults to 1 day) * - FLEET_OPERATOR: Address of the Nodle swarm operator (initial whitelisted user) + * - FLEET_OPERATOR_BOND_ALLOWANCE: (optional) Initial bond allowance for fleet operator in wei (defaults to BOND_QUOTA) */ contract DeploySwarmUpgradeableZkSync is Script { // Deployment artifacts @@ -50,6 +51,7 @@ contract DeploySwarmUpgradeableZkSync is Script { uint256 bondQuota = vm.envOr("BOND_QUOTA", uint256(100_000 ether)); // 100k NODL default uint256 bondPeriod = vm.envOr("BOND_PERIOD", uint256(1 days)); address fleetOperator = vm.envAddress("FLEET_OPERATOR"); + uint256 fleetOperatorBondAllowance = vm.envOr("FLEET_OPERATOR_BOND_ALLOWANCE", bondQuota); console.log("=== Deploying Upgradeable Swarm Contracts on ZkSync ==="); console.log("Bond Token:", bondToken); @@ -59,6 +61,7 @@ contract DeploySwarmUpgradeableZkSync is Script { console.log("Bond Quota:", bondQuota); console.log("Bond Period:", bondPeriod); console.log("Fleet Operator:", fleetOperator); + console.log("Fleet Operator Bond Allowance:", fleetOperatorBondAllowance); console.log(""); vm.startBroadcast(deployerPrivateKey); @@ -105,6 +108,8 @@ contract DeploySwarmUpgradeableZkSync is Script { whitelistedContracts[2] = swarmRegistryProxy; address[] memory whitelistedUsers = new address[](1); whitelistedUsers[0] = fleetOperator; + uint256[] memory initialBondAllowances = new uint256[](1); + initialBondAllowances[0] = fleetOperatorBondAllowance; bondTreasuryPaymaster = address( new BondTreasuryPaymaster( owner, @@ -112,6 +117,7 @@ contract DeploySwarmUpgradeableZkSync is Script { withdrawer, whitelistedContracts, whitelistedUsers, + initialBondAllowances, bondToken, bondQuota, bondPeriod diff --git a/src/paymasters/BondTreasuryPaymaster.sol b/src/paymasters/BondTreasuryPaymaster.sol index 213f8ba..2c33644 100644 --- a/src/paymasters/BondTreasuryPaymaster.sol +++ b/src/paymasters/BondTreasuryPaymaster.sol @@ -27,6 +27,7 @@ contract BondTreasuryPaymaster is WhitelistPaymaster, QuotaControl { error CallerNotWhitelistedContract(); error InsufficientBondBalance(); error UserBondAllowanceExceeded(); + error ArrayLengthMismatch(); constructor( address admin, @@ -34,10 +35,12 @@ contract BondTreasuryPaymaster is WhitelistPaymaster, QuotaControl { address withdrawer, address[] memory initialWhitelistedContracts, address[] memory initialWhitelistedUsers, + uint256[] memory initialBondAllowances, address bondToken_, uint256 initialQuota, uint256 initialPeriod ) WhitelistPaymaster(admin, withdrawer) QuotaControl(initialQuota, initialPeriod, admin) { + if (initialWhitelistedUsers.length != initialBondAllowances.length) revert ArrayLengthMismatch(); if (whitelistAdmin != admin) { _grantRole(WHITELIST_ADMIN_ROLE, whitelistAdmin); } @@ -61,6 +64,7 @@ contract BondTreasuryPaymaster is WhitelistPaymaster, QuotaControl { uint256 m = initialWhitelistedUsers.length; for (uint256 j = 0; j < m; ++j) { isWhitelistedUser[initialWhitelistedUsers[j]] = true; + userBondAllowance[initialWhitelistedUsers[j]] = initialBondAllowances[j]; } if (m > 0) { emit WhitelistedUsersAdded(initialWhitelistedUsers); diff --git a/test/paymasters/BondTreasuryPaymaster.t.sol b/test/paymasters/BondTreasuryPaymaster.t.sol index 864a92f..d689ec6 100644 --- a/test/paymasters/BondTreasuryPaymaster.t.sol +++ b/test/paymasters/BondTreasuryPaymaster.t.sol @@ -46,6 +46,7 @@ contract MockBondTreasuryPaymaster is BondTreasuryPaymaster { address withdrawer, address[] memory initialWhitelistedContracts, address[] memory initialWhitelistedUsers, + uint256[] memory initialBondAllowances, address bondToken_, uint256 initialQuota, uint256 initialPeriod @@ -56,6 +57,7 @@ contract MockBondTreasuryPaymaster is BondTreasuryPaymaster { withdrawer, initialWhitelistedContracts, initialWhitelistedUsers, + initialBondAllowances, bondToken_, initialQuota, initialPeriod @@ -111,12 +113,22 @@ contract BondTreasuryPaymasterTest is Test { return new address[](0); } + function _emptyAmounts() internal pure returns (uint256[] memory) { + return new uint256[](0); + } + function _singleAddress(address a) internal pure returns (address[] memory) { address[] memory arr = new address[](1); arr[0] = a; return arr; } + function _singleAmount(uint256 v) internal pure returns (uint256[] memory) { + uint256[] memory arr = new uint256[](1); + arr[0] = v; + return arr; + } + function setUp() public { bondToken = new MockERC20SCP(); @@ -131,12 +143,17 @@ contract BondTreasuryPaymasterTest is Test { initialUsers[0] = alice; initialUsers[1] = admin; + uint256[] memory initialAllowances = new uint256[](2); + initialAllowances[0] = 10_000 ether; + initialAllowances[1] = 10_000 ether; + paymaster = new MockBondTreasuryPaymaster( admin, admin, withdrawer, _initialContractWhitelist(address(fleet)), initialUsers, + initialAllowances, address(bondToken), QUOTA, PERIOD @@ -145,10 +162,6 @@ contract BondTreasuryPaymasterTest is Test { bondToken.mint(address(paymaster), 10_000 ether); whitelistTargets = new address[](1); whitelistTargets[0] = alice; - - // Give alice a generous bond allowance for existing tests - vm.prank(admin); - paymaster.setUserBondAllowance(alice, 10_000 ether); } // ══════════════════════════════════════════════ @@ -169,6 +182,7 @@ contract BondTreasuryPaymasterTest is Test { withdrawer, _initialContractWhitelist(address(fleet)), _emptyAddresses(), + _emptyAmounts(), address(bondToken), QUOTA, PERIOD @@ -197,6 +211,11 @@ contract BondTreasuryPaymasterTest is Test { assertTrue(paymaster.isWhitelistedUser(admin)); } + function test_initialBondAllowancesSetInConstructor() public view { + assertEq(paymaster.userBondAllowance(alice), 10_000 ether); + assertEq(paymaster.userBondAllowance(admin), 10_000 ether); + } + function test_constructorWithEmptyWhitelistedUsers() public { MockBondTreasuryPaymaster pm = new MockBondTreasuryPaymaster( admin, @@ -204,6 +223,7 @@ contract BondTreasuryPaymasterTest is Test { withdrawer, _initialContractWhitelist(address(fleet)), _emptyAddresses(), + _emptyAmounts(), address(bondToken), QUOTA, PERIOD @@ -219,12 +239,18 @@ contract BondTreasuryPaymasterTest is Test { users[1] = bob; users[2] = charlie; + uint256[] memory allowances = new uint256[](3); + allowances[0] = 500 ether; + allowances[1] = 300 ether; + allowances[2] = 100 ether; + MockBondTreasuryPaymaster pm = new MockBondTreasuryPaymaster( admin, admin, withdrawer, _initialContractWhitelist(address(fleet)), users, + allowances, address(bondToken), QUOTA, PERIOD @@ -232,6 +258,9 @@ contract BondTreasuryPaymasterTest is Test { assertTrue(pm.isWhitelistedUser(alice)); assertTrue(pm.isWhitelistedUser(bob)); assertTrue(pm.isWhitelistedUser(charlie)); + assertEq(pm.userBondAllowance(alice), 500 ether); + assertEq(pm.userBondAllowance(bob), 300 ether); + assertEq(pm.userBondAllowance(charlie), 100 ether); } function test_constructorEmitsWhitelistedUsersAdded() public { @@ -239,6 +268,10 @@ contract BondTreasuryPaymasterTest is Test { users[0] = alice; users[1] = bob; + uint256[] memory allowances = new uint256[](2); + allowances[0] = 100 ether; + allowances[1] = 200 ether; + vm.expectEmit(); emit WhitelistPaymaster.WhitelistedUsersAdded(users); new MockBondTreasuryPaymaster( @@ -247,6 +280,7 @@ contract BondTreasuryPaymasterTest is Test { withdrawer, _initialContractWhitelist(address(fleet)), users, + allowances, address(bondToken), QUOTA, PERIOD @@ -261,6 +295,7 @@ contract BondTreasuryPaymasterTest is Test { withdrawer, _initialContractWhitelist(address(fleet)), _emptyAddresses(), + _emptyAmounts(), address(bondToken), QUOTA, PERIOD @@ -272,6 +307,80 @@ contract BondTreasuryPaymasterTest is Test { } } + function test_RevertIf_constructorArrayLengthMismatch_moreThanUsers() public { + uint256[] memory tooMany = new uint256[](2); + tooMany[0] = 100 ether; + tooMany[1] = 200 ether; + + vm.expectRevert(BondTreasuryPaymaster.ArrayLengthMismatch.selector); + new MockBondTreasuryPaymaster( + admin, + admin, + withdrawer, + _initialContractWhitelist(address(fleet)), + _singleAddress(alice), + tooMany, + address(bondToken), + QUOTA, + PERIOD + ); + } + + function test_RevertIf_constructorArrayLengthMismatch_fewerThanUsers() public { + address[] memory users = new address[](2); + users[0] = alice; + users[1] = bob; + + vm.expectRevert(BondTreasuryPaymaster.ArrayLengthMismatch.selector); + new MockBondTreasuryPaymaster( + admin, + admin, + withdrawer, + _initialContractWhitelist(address(fleet)), + users, + _singleAmount(100 ether), + address(bondToken), + QUOTA, + PERIOD + ); + } + + function test_constructorZeroAllowance_whitelistedButNoAllowance() public { + MockBondTreasuryPaymaster pm = new MockBondTreasuryPaymaster( + admin, + admin, + withdrawer, + _initialContractWhitelist(address(fleet)), + _singleAddress(alice), + _singleAmount(0), + address(bondToken), + QUOTA, + PERIOD + ); + assertTrue(pm.isWhitelistedUser(alice)); + assertEq(pm.userBondAllowance(alice), 0); + } + + function test_constructorAllowance_usableImmediately() public { + MockBondTreasuryPaymaster pm = new MockBondTreasuryPaymaster( + admin, + admin, + withdrawer, + _initialContractWhitelist(address(fleet)), + _singleAddress(alice), + _singleAmount(BASE_BOND), + address(bondToken), + QUOTA, + PERIOD + ); + bondToken.mint(address(pm), 10_000 ether); + + vm.prank(alice); + fleet.claimUuidSponsored(UUID_1, address(0), address(pm)); + assertEq(fleet.uuidOwner(UUID_1), alice); + assertEq(pm.userBondAllowance(alice), 0); + } + // ══════════════════════════════════════════════ // Whitelist Management // ══════════════════════════════════════════════ @@ -553,14 +662,13 @@ contract BondTreasuryPaymasterTest is Test { withdrawer, _initialContractWhitelist(address(fleet)), _singleAddress(alice), + _singleAmount(10_000 ether), address(bondToken), BASE_BOND / 2, PERIOD ); bondToken.mint(address(tightPaymaster), 10_000 ether); - vm.prank(admin); - tightPaymaster.setUserBondAllowance(alice, 10_000 ether); vm.prank(alice); vm.expectRevert(QuotaControl.QuotaExceeded.selector); @@ -638,6 +746,7 @@ contract BondTreasuryPaymasterTest is Test { withdrawer, _initialContractWhitelist(address(fleet)), _emptyAddresses(), + _emptyAmounts(), address(bondToken), QUOTA, 0 @@ -652,6 +761,7 @@ contract BondTreasuryPaymasterTest is Test { withdrawer, _initialContractWhitelist(address(fleet)), _emptyAddresses(), + _emptyAmounts(), address(bondToken), QUOTA, 31 days