Skip to content

Commit

Permalink
Add fee multiplier to protocol (Snowfork#1161)
Browse files Browse the repository at this point in the history
* Add fee multiplier

* The base fee and exchange rate should be multiplied separately

* Add FEE_MULTIPLIER to E2E config

* Update web/packages/test/scripts/set-env.sh

Co-authored-by: Alistair Singh <[email protected]>

* Make multiplier non-linear

* Add gateway upgrade for Rococo

---------

Co-authored-by: Alistair Singh <[email protected]>
  • Loading branch information
vgeddes and alistair-singh authored Mar 21, 2024
1 parent 8a51ac9 commit cc6f123
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 20 deletions.
3 changes: 2 additions & 1 deletion contracts/src/DeployScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
25 changes: 16 additions & 9 deletions contracts/src/Gateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions contracts/src/Params.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions contracts/src/storage/PricingStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
33 changes: 33 additions & 0 deletions contracts/src/upgrades/rococo/GatewayV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <[email protected]>
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));
}
}
24 changes: 16 additions & 8 deletions contracts/test/Gateway.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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 =
Expand All @@ -497,7 +499,8 @@ contract GatewayTest is Test {
assetHubAgentID: assetHubAgentID,
assetHubCreateAssetFee: createTokenFee,
assetHubReserveTransferFee: sendTokenFee,
exchangeRate: exchangeRate
exchangeRate: exchangeRate,
multiplier: multiplier
});

UpgradeParams memory params = UpgradeParams({
Expand All @@ -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));
Expand All @@ -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));
}

Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions web/packages/test/scripts/set-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit cc6f123

Please sign in to comment.