diff --git a/contracts/src/DeployScript.sol b/contracts/src/DeployScript.sol index 77fb75e46..8290d9ff8 100644 --- a/contracts/src/DeployScript.sol +++ b/contracts/src/DeployScript.sol @@ -80,7 +80,8 @@ contract DeployScript is Script { assetHubAgentID: assetHubAgentID, assetHubCreateAssetFee: uint128(vm.envUint("CREATE_ASSET_FEE")), assetHubReserveTransferFee: uint128(vm.envUint("RESERVE_TRANSFER_FEE")), - exchangeRate: ud60x18(vm.envUint("EXCHANGE_RATE")) + exchangeRate: ud60x18(vm.envUint("EXCHANGE_RATE")), + multiplier: ud60x18(vm.envUint("FEE_MULTIPLIER")) }); GatewayProxy gateway = new GatewayProxy(address(gatewayLogic), abi.encode(config)); diff --git a/contracts/src/Gateway.sol b/contracts/src/Gateway.sol index 60f5d8c32..4f237d35f 100644 --- a/contracts/src/Gateway.sol +++ b/contracts/src/Gateway.sol @@ -390,6 +390,7 @@ contract Gateway is IGateway, IInitializable { SetPricingParametersParams memory params = abi.decode(data, (SetPricingParametersParams)); pricing.exchangeRate = params.exchangeRate; pricing.deliveryCost = params.deliveryCost; + pricing.multiplier = params.multiplier; emit PricingParametersChanged(); } @@ -462,19 +463,22 @@ contract Gateway is IGateway, IInitializable { } // Convert foreign currency to native currency (ROC/KSM/DOT -> ETH) - function _convertToNative(UD60x18 exchangeRate, uint256 amount) internal view returns (uint256) { - UD60x18 amountFP = convert(amount); + function _convertToNative(UD60x18 exchangeRate, UD60x18 multiplier, UD60x18 amount) + internal + view + returns (uint256) + { UD60x18 ethDecimals = convert(1e18); UD60x18 foreignDecimals = convert(10).pow(convert(uint256(FOREIGN_TOKEN_DECIMALS))); - UD60x18 nativeAmountFP = amountFP.mul(exchangeRate).div(foreignDecimals).mul(ethDecimals); - uint256 nativeAmount = convert(nativeAmountFP); - return nativeAmount; + UD60x18 nativeAmount = multiplier.mul(amount).mul(exchangeRate).div(foreignDecimals).mul(ethDecimals); + return convert(nativeAmount); } // Calculate the fee for accepting an outbound message function _calculateFee(Costs memory costs) internal view returns (uint256) { PricingStorage.Layout storage pricing = PricingStorage.layout(); - return costs.native + _convertToNative(pricing.exchangeRate, pricing.deliveryCost + costs.foreign); + UD60x18 amount = convert(pricing.deliveryCost + costs.foreign); + return costs.native + _convertToNative(pricing.exchangeRate, pricing.multiplier, amount); } // Submit an outbound message to Polkadot, after taking fees @@ -569,24 +573,26 @@ contract Gateway is IGateway, IInitializable { uint128 assetHubReserveTransferFee; /// @dev extra fee to discourage spamming uint256 registerTokenFee; + /// @dev Fee multiplier + UD60x18 multiplier; } /// @dev Initialize storage in the gateway /// NOTE: This is not externally accessible as this function selector is overshadowed in the proxy - function initialize(bytes calldata data) external { + function initialize(bytes calldata data) external virtual { // Prevent initialization of storage in implementation contract if (ERC1967.load() == address(0)) { revert Unauthorized(); } - Config memory config = abi.decode(data, (Config)); - CoreStorage.Layout storage core = CoreStorage.layout(); if (core.channels[PRIMARY_GOVERNANCE_CHANNEL_ID].agent != address(0)) { revert AlreadyInitialized(); } + Config memory config = abi.decode(data, (Config)); + core.mode = config.mode; // Initialize agent for BridgeHub @@ -613,6 +619,7 @@ contract Gateway is IGateway, IInitializable { PricingStorage.Layout storage pricing = PricingStorage.layout(); pricing.exchangeRate = config.exchangeRate; pricing.deliveryCost = config.deliveryCost; + pricing.multiplier = config.multiplier; // Initialize assets storage AssetsStorage.Layout storage assets = AssetsStorage.layout(); diff --git a/contracts/src/Params.sol b/contracts/src/Params.sol index 2c424949f..fc0298915 100644 --- a/contracts/src/Params.sol +++ b/contracts/src/Params.sol @@ -79,4 +79,6 @@ struct SetPricingParametersParams { UD60x18 exchangeRate; /// @dev The cost of delivering messages to BridgeHub in DOT uint128 deliveryCost; + /// @dev Fee multiplier + UD60x18 multiplier; } diff --git a/contracts/src/storage/PricingStorage.sol b/contracts/src/storage/PricingStorage.sol index 30e6f8f42..d852e7b9a 100644 --- a/contracts/src/storage/PricingStorage.sol +++ b/contracts/src/storage/PricingStorage.sol @@ -10,6 +10,8 @@ library PricingStorage { UD60x18 exchangeRate; /// @dev The cost of delivering messages to BridgeHub in DOT uint128 deliveryCost; + /// @dev Fee multiplier + UD60x18 multiplier; } bytes32 internal constant SLOT = keccak256("org.snowbridge.storage.pricing"); diff --git a/contracts/src/upgrades/rococo/GatewayV2.sol b/contracts/src/upgrades/rococo/GatewayV2.sol new file mode 100644 index 000000000..3918e3d79 --- /dev/null +++ b/contracts/src/upgrades/rococo/GatewayV2.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +pragma solidity 0.8.23; + +import "../../Gateway.sol"; + +import {UD60x18, convert} from "prb/math/src/UD60x18.sol"; +import {PricingStorage} from "../../storage/PricingStorage.sol"; + +contract GatewayV2 is Gateway { + constructor( + address beefyClient, + address agentExecutor, + ParaID bridgeHubParaID, + bytes32 bridgeHubAgentID, + uint8 foreignTokenDecimals + ) Gateway(beefyClient, agentExecutor, bridgeHubParaID, bridgeHubAgentID, foreignTokenDecimals) {} + + function initialize(bytes memory data) external override { + // Prevent initialization of storage in implementation contract + if (ERC1967.load() == address(0)) { + revert Unauthorized(); + } + + PricingStorage.Layout storage pricing = PricingStorage.layout(); + + if (pricing.multiplier != convert(0)) { + revert AlreadyInitialized(); + } + + pricing.multiplier = abi.decode(data, (UD60x18)); + } +} diff --git a/contracts/test/Gateway.t.sol b/contracts/test/Gateway.t.sol index 604e1a9d3..287f059a6 100644 --- a/contracts/test/Gateway.t.sol +++ b/contracts/test/Gateway.t.sol @@ -90,6 +90,7 @@ contract GatewayTest is Test { // ETH/DOT exchange rate UD60x18 public exchangeRate = ud60x18(0.0025e18); + UD60x18 public multiplier = ud60x18(1e18); function setUp() public { AgentExecutor executor = new AgentExecutor(); @@ -103,7 +104,8 @@ contract GatewayTest is Test { assetHubAgentID: assetHubAgentID, assetHubCreateAssetFee: createTokenFee, assetHubReserveTransferFee: sendTokenFee, - exchangeRate: exchangeRate + exchangeRate: exchangeRate, + multiplier: multiplier }); gateway = new GatewayProxy(address(gatewayLogic), abi.encode(config)); GatewayMock(address(gateway)).setCommitmentsAreVerified(true); @@ -483,7 +485,7 @@ contract GatewayTest is Test { assertEq(GatewayV2(address(gateway)).getValue(), 42); } - function testUgradeInitializerRunsOnlyOnce() public { + function testUpgradeInitializerRunsOnlyOnce() public { // Upgrade to this current logic contract AgentExecutor executor = new AgentExecutor(); GatewayMock currentLogic = @@ -497,7 +499,8 @@ contract GatewayTest is Test { assetHubAgentID: assetHubAgentID, assetHubCreateAssetFee: createTokenFee, assetHubReserveTransferFee: sendTokenFee, - exchangeRate: exchangeRate + exchangeRate: exchangeRate, + multiplier: multiplier }); UpgradeParams memory params = UpgradeParams({ @@ -516,7 +519,7 @@ contract GatewayTest is Test { testSetPricingParameters(); uint256 fee = IGateway(address(gateway)).quoteRegisterTokenFee(); - assertEq(fee, 10000000000000000); + assertEq(fee, 20000000000000001); testCreateAgent(); assertNotEq(GatewayMock(address(gateway)).agentOf(agentID), address(0)); @@ -538,7 +541,7 @@ contract GatewayTest is Test { // Verify that storage was not overwritten fee = IGateway(address(gateway)).quoteRegisterTokenFee(); - assertEq(fee, 10000000000000000); + assertEq(fee, 20000000000000001); assertNotEq(GatewayMock(address(gateway)).agentOf(agentID), address(0)); } @@ -875,14 +878,19 @@ contract GatewayTest is Test { function testSetPricingParameters() public { uint256 fee = IGateway(address(gateway)).quoteRegisterTokenFee(); assertEq(fee, 5000000000000000); - // Double the exchangeRate + // Double both the exchangeRate and multiplier. Should lead to an 4x fee increase GatewayMock(address(gateway)).setPricingParametersPublic( abi.encode( - SetPricingParametersParams({exchangeRate: exchangeRate.mul(convert(2)), deliveryCost: outboundFee}) + SetPricingParametersParams({ + exchangeRate: exchangeRate.mul(convert(2)), + multiplier: multiplier.mul(convert(2)), + deliveryCost: outboundFee + }) ) ); + // Should expect 4x fee increase fee = IGateway(address(gateway)).quoteRegisterTokenFee(); - assertEq(fee, 10000000000000000); + assertEq(fee, 20000000000000001); } function testSendTokenToForeignDestWithInvalidFee() public { diff --git a/web/packages/test/scripts/set-env.sh b/web/packages/test/scripts/set-env.sh index 494f9b5b3..81a86ff4d 100755 --- a/web/packages/test/scripts/set-env.sh +++ b/web/packages/test/scripts/set-env.sh @@ -94,12 +94,13 @@ export REJECT_OUTBOUND_MESSAGES="${REJECT_OUTBOUND_MESSAGES:-false}" ## Fee export REGISTER_TOKEN_FEE="${REGISTER_TOKEN_FEE:-200000000000000000}" -export DELIVERY_COST="${DELIVERY_COST:-10000000000}" export CREATE_ASSET_FEE="${CREATE_ASSET_FEE:-10000000000}" export RESERVE_TRANSFER_FEE="${RESERVE_TRANSFER_FEE:-10000000000}" -## Price +## Pricing Parameters export EXCHANGE_RATE="${EXCHANGE_RATE:-2500000000000000}" +export DELIVERY_COST="${DELIVERY_COST:-10000000000}" +export FEE_MULTIPLIER="${FEE_MULTIPLIER:-1000000000000000000}" export FEE_PER_GAS="${FEE_PER_GAS:-20000000000}" ## Reward