Skip to content

Commit

Permalink
Bridge: Minimum transfer amount requirement
Browse files Browse the repository at this point in the history
  • Loading branch information
kacperzuk-neti committed Jul 26, 2023
1 parent e8de720 commit 6fad183
Show file tree
Hide file tree
Showing 10 changed files with 83 additions and 23 deletions.
4 changes: 4 additions & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,7 @@ parameter_types! {
60 * DOLLARS
);
pub const LLDMaxTotalLocked: Balance = 300_000 * DOLLARS;
pub const LLDMinimumTransfer: Balance = 30 * DOLLARS;

// * 22k LLM in single hour (1 burst + hour avg)
// * 298k LLM in single day (1 burst + 24*hour avg)
Expand All @@ -1573,6 +1574,7 @@ parameter_types! {
20 * GRAINS_IN_LLM
);
pub const LLMMaxTotalLocked: Balance = 100_000 * GRAINS_IN_LLM;
pub const LLMMinimumTransfer: Balance = 10 * GRAINS_IN_LLM;
}

type EthLLDBridgeInstance = pallet_federated_bridge::Instance1;
Expand All @@ -1587,6 +1589,7 @@ impl pallet_federated_bridge::Config<EthLLDBridgeInstance> for Runtime {
type WithdrawalDelay = WithdrawalDelay;
type WithdrawalRateLimit = LLDRateLimit;
type MaxTotalLocked = LLDMaxTotalLocked;
type MinimumTransfer = LLDMinimumTransfer;
type WeightInfo = ();
}

Expand All @@ -1602,6 +1605,7 @@ impl pallet_federated_bridge::Config<EthLLMBridgeInstance> for Runtime {
type WithdrawalDelay = WithdrawalDelay;
type WithdrawalRateLimit = LLMRateLimit;
type MaxTotalLocked = LLMMaxTotalLocked;
type MinimumTransfer = LLMMinimumTransfer;
type WeightInfo = ();
}

Expand Down
7 changes: 4 additions & 3 deletions eth-bridge/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,19 @@ Following security features are implemented in the contract:
recipient etc)
* bridge can be stopped by a watcher at any time
* bridge enforces rate-limit on mints
* bridge limits how many wrapped tokens can in circulation
* bridge enforces minimum transfer amount
* bridge limits how many wrapped tokens can be in circulation
* there's a delay between approval of mint and actually allowing it

## Official deployments

Sepolia testnet (meant to be used with Liberland Bastiat testnet):

```
LDN ERC-20 token: 0x195E89cdE9475e18DB594A24FC2954235d7F4e92
LDN ERC-20 token: 0x0f018D7e0B8f5D5cCc88c0B23d931AaAA13B0C42
LDN Bridge (proxy): 0xC8af0C3E0e4FC787D9e657b2A68ce6ED9cedB5DA
LKN ERC-20 token: 0x023A1D6C9f4b792399804DF2AaFaA2Df2bdd8a12
LKN ERC-20 token: 0x7134B5DF53D7A276849a1A64a76f6D8972508747
LKN Bridge (proxy): 0x7E1E09c0B41b22EB1fA04145cdf21cea01560c99
Common bridge implementation: 0x14cb6EDb15c156272B59e7Fd688a1F0b09C999d4
Expand Down
8 changes: 5 additions & 3 deletions eth-bridge/contracts/script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ contract Deploy is Script {
delay,
fee,
30_000_000_000_000_000, // max burst mint
60_000_000_000_000, // rate limit counter decay
300_000_000_000_000_000 // max total supply
60_000_000_000_000, // rate limit counter decay
300_000_000_000_000_000, // max total supply
30_000_000_000_000 // min transfer
)
)
);
Expand All @@ -50,7 +51,8 @@ contract Deploy is Script {
fee,
10_000_000_000_000_000, // max burst mint
20_000_000_000_000, // rate limit counter decay
100_000_000_000_000_000 // max total supply
100_000_000_000_000_000, // max total supply
10_000_000_000_000 // min transfer
)
)
);
Expand Down
17 changes: 16 additions & 1 deletion eth-bridge/contracts/src/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ error InvalidArgument();
error InvalidConfiguration();
/// This relay already voted for this ReceiptId
error AlreadyVoted();
/// Transferred amount is less than configured minimum
error TooSmallAmount();

/// @title Interface with events emitted by the bridge
interface BridgeEvents {
Expand Down Expand Up @@ -144,6 +146,8 @@ contract Bridge is Initializable, AccessControlUpgradeable, UUPSUpgradeable, Bri
RateLimitCounter public mintCounter; // 2x uint256
/// Rate limit configuration
RateLimitParameters public rateLimit; // 2x uint256
/// Minimum transfer - only applied for burns
uint256 public minTransfer;

constructor() {
_disableInitializers();
Expand All @@ -157,14 +161,16 @@ contract Bridge is Initializable, AccessControlUpgradeable, UUPSUpgradeable, Bri
/// @param counterLimit initial `rateLimit.counterLimit`
/// @param decayRate initial `rateLimit.decayRate`
/// @param supplyLimit_ initial `supplyLimit`
/// @param minTransfer_ initial `minTransfer`
function initialize(
WrappedToken token_,
uint32 votesRequired_,
uint256 mintDelay_,
uint256 fee_,
uint256 counterLimit,
uint256 decayRate,
uint256 supplyLimit_
uint256 supplyLimit_,
uint256 minTransfer_
) public initializer {
__AccessControl_init();
__UUPSUpgradeable_init();
Expand All @@ -177,6 +183,7 @@ contract Bridge is Initializable, AccessControlUpgradeable, UUPSUpgradeable, Bri
fee = fee_;
mintDelay = mintDelay_;
supplyLimit = supplyLimit_;
minTransfer = minTransfer_;
// slither-disable-end events-maths
_grantRole(SUPER_ADMIN_ROLE, msg.sender);
}
Expand Down Expand Up @@ -238,6 +245,7 @@ contract Bridge is Initializable, AccessControlUpgradeable, UUPSUpgradeable, Bri
function burn(uint256 amount, bytes32 substrateRecipient) public {
// CHECKS
if (!bridgeActive) revert BridgeInactive();
if (amount < minTransfer) revert TooSmallAmount();

// EFFECTS
emit OutgoingReceipt(msg.sender, substrateRecipient, amount);
Expand Down Expand Up @@ -411,6 +419,13 @@ contract Bridge is Initializable, AccessControlUpgradeable, UUPSUpgradeable, Bri
supplyLimit = supplyLimit_;
}

/// Set minimum transfer amount
/// @param minTransfer_ New minimum transfer amount
/// @dev Only addresses with ADMIN_ROLE can call this
function setMinTransfer(uint256 minTransfer_) public onlyRole(ADMIN_ROLE) {
minTransfer = minTransfer_;
}

/// Transfer ownership of underlying token contract.
/// Will stop the bridge and set the token address to 0, effectively
/// bricking the bridge.
Expand Down
14 changes: 13 additions & 1 deletion eth-bridge/contracts/test/Bridge.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ contract BridgeTest is Test, BridgeEvents {
4,
1000,
10,
650
650,
0
)
)
)
Expand Down Expand Up @@ -856,4 +857,15 @@ contract BridgeTest is Test, BridgeEvents {
assertEq(bob.balance, 5);
assertEq(charlie.balance, 3);
}

function testMinTransferIsRespected() public {
vm.startPrank(dave);
bridge.setMinTransfer(10);

vm.expectRevert(TooSmallAmount.selector);
bridge.burn(9, substrate1);

bridge.setMinTransfer(9);
bridge.burn(9, substrate1);
}
}
3 changes: 2 additions & 1 deletion eth-bridge/contracts/test/VoteFeeCompare.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ contract BridgeTest is Test, BridgeEvents {
0,
0,
0,
100
100,
0
)
)
)
Expand Down
2 changes: 2 additions & 0 deletions frame/federated-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Following security features are implemented substrate-side:
* bridge doesn't mint any new funds - only funds stored in bridge (a.k.a. existing as wrapped
tokens on Eth side) are at risk
* bridge enforces rate-limit on withdrawals
* bridge enforces minimum transfer amount
* bridge limits how many tokens can be locked (bridged) at the same time
* there's a delay between approval of withdrawal and actually allowing it

Expand All @@ -95,6 +96,7 @@ Following security features are implemented substrate-side:
will start failing after this is reached.
* `WithdrawalDelay` - number of blocks between transfer approval and actually allowing it
* `WithdrawalRateLimit` - rate limit parameters
* `MinimumTransfer` - minimum amount that can be deposited in single call
* `ForceOrigin` - origin that's authorized to set admin and super admin


Expand Down
11 changes: 11 additions & 0 deletions frame/federated-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
//! * bridge doesn't mint any new funds - only funds stored in bridge (a.k.a. existing as wrapped
//! tokens on Eth side) are at risk
//! * bridge enforces rate-limit on withdrawals
//! * bridge enforces minimum transfer amount
//! * bridge limits how many tokens can be locked (bridged) at the same time
//! * there's a delay between approval of withdrawal and actually allowing it
//!
Expand All @@ -95,6 +96,7 @@
//! will start failing after this is reached.
//! * `WithdrawalDelay` - number of blocks between transfer approval and actually allowing it
//! * `WithdrawalRateLimit` - rate limit parameters
//! * `MinimumTransfer` - minimum amount that can be deposited in single call
//! * `ForceOrigin` - origin that's authorized to set admin and super admin
//!
//!
Expand Down Expand Up @@ -254,6 +256,11 @@ pub mod pallet {
/// much can be withdrawn per block)
type WithdrawalRateLimit: Get<(BalanceOfToken<Self, I>, BalanceOfToken<Self, I>)>;

#[pallet::constant]
/// Minimum amount that has to be bridged in a single transfer. Only
/// enforced on deposits.
type MinimumTransfer: Get<BalanceOfToken<Self, I>>;

/// Origin that's authorized to set Admin and SuperAdmin
type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;

Expand Down Expand Up @@ -291,6 +298,8 @@ pub mod pallet {
RateLimited,
/// Too much locked in pallet already
TooMuchLocked,
/// Amount smaller than MinimumTransfer
TooSmallAmount,
}

#[pallet::event]
Expand Down Expand Up @@ -445,6 +454,7 @@ pub mod pallet {
/// Can be called by any Signed origin.
///
/// Fails if bridge is stopped or caller has insufficient funds.
/// Fails with `TooSmallAmount` amount is smaller than MinimumTransfer.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::deposit())]
pub fn deposit(
Expand All @@ -455,6 +465,7 @@ pub mod pallet {
let who = ensure_signed(origin)?;

ensure!(State::<T, I>::get() == BridgeState::Active, Error::<T, I>::BridgeStopped);
ensure!(amount >= T::MinimumTransfer::get(), Error::<T, I>::TooSmallAmount);

let total_locked = <T::Token as Inspect<T::AccountId>>::balance(&Self::account_id());
ensure!(
Expand Down
1 change: 1 addition & 0 deletions frame/federated-bridge/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ impl pallet_federated_bridge::Config for Test {
type WithdrawalRateLimit = RateLimit;
type ForceOrigin = EnsureRoot<Self::AccountId>;
type MaxTotalLocked = ConstU64<10000>;
type MinimumTransfer = ConstU64<2>;
type WeightInfo = ();
}

Expand Down
39 changes: 25 additions & 14 deletions frame/federated-bridge/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,30 +40,30 @@ fn deposit_fails_on_stopped_bridge() {
#[test]
fn deposit_emits_receipt() {
new_test_ext().execute_with(|| {
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 1, eth_recipient(0)));
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 2, eth_recipient(0)));
System::assert_last_event(
Event::<Test>::OutgoingReceipt { from: 0, amount: 1, eth_recipient: eth_recipient(0) }.into(),
Event::<Test>::OutgoingReceipt { from: 0, amount: 2, eth_recipient: eth_recipient(0) }.into(),
);
});
}

#[test]
fn deposit_takes_token_from_caller() {
new_test_ext().execute_with(|| {
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 1, eth_recipient(0)));
assert_eq!(Balances::free_balance(0), 99);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 2, eth_recipient(0)));
assert_eq!(Balances::free_balance(0), 98);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 90, eth_recipient(0)));
assert_eq!(Balances::free_balance(0), 9);
assert_eq!(Balances::free_balance(0), 8);
});
}

#[test]
fn deposit_stores_token_in_bridge() {
new_test_ext().execute_with(|| {
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 1, eth_recipient(0)));
assert_eq!(Balances::free_balance(bridge_wallet()), 1);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 2, eth_recipient(0)));
assert_eq!(Balances::free_balance(bridge_wallet()), 2);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 90, eth_recipient(0)));
assert_eq!(Balances::free_balance(bridge_wallet()), 91);
assert_eq!(Balances::free_balance(bridge_wallet()), 92);
});
}

Expand Down Expand Up @@ -91,12 +91,23 @@ fn deposit_respects_max_total_locked() {
Bridge::deposit(RuntimeOrigin::signed(200), 10001, eth_recipient(0)),
Error::<Test>::TooMuchLocked
);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 9999, eth_recipient(0)));
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 9998, eth_recipient(0)));
assert_noop!(
Bridge::deposit(RuntimeOrigin::signed(200), 2, eth_recipient(0)),
Bridge::deposit(RuntimeOrigin::signed(200), 3, eth_recipient(0)),
Error::<Test>::TooMuchLocked
);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 1, eth_recipient(0)));
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 2, eth_recipient(0)));
});
}

#[test]
fn deposit_respects_minimum_transfer() {
new_test_ext().execute_with(|| {
assert_noop!(
Bridge::deposit(RuntimeOrigin::signed(200), 1, eth_recipient(0)),
Error::<Test>::TooSmallAmount
);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 2, eth_recipient(0)));
});
}

Expand Down Expand Up @@ -147,7 +158,7 @@ fn vote_succeeds_even_after_reaching_required_votes() {
fn vote_fails_on_processed_receipt() {
new_test_ext().execute_with(|| {
let (receipt_id, receipt) = gen_receipt(0, 1);
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 1, eth_recipient(0)));
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(0), 2, eth_recipient(0)));
assert_ok!(Bridge::vote_withdraw(RuntimeOrigin::signed(0), receipt_id, receipt.clone()));
assert_ok!(Bridge::vote_withdraw(RuntimeOrigin::signed(1), receipt_id, receipt.clone()));
System::set_block_number(11);
Expand Down Expand Up @@ -926,9 +937,9 @@ fn max_total_locked_is_respected_after_withdrawals() {
);
assert_ok!(Bridge::withdraw(RuntimeOrigin::signed(0), receipt_id));
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 9001, eth_recipient(0)));
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 999, eth_recipient(0)));
assert_ok!(Bridge::deposit(RuntimeOrigin::signed(200), 998, eth_recipient(0)));
assert_noop!(
Bridge::deposit(RuntimeOrigin::signed(0), 1, eth_recipient(0)),
Bridge::deposit(RuntimeOrigin::signed(0), 2, eth_recipient(0)),
Error::<Test>::TooMuchLocked
);
});
Expand Down

0 comments on commit 6fad183

Please sign in to comment.