Berachain protocol analysis - Infrared
Berachain protocol analysis - Infrared
The code is quite long, so I’ve included GitHub links in the Reference section. It would be beneficial to refer to them while reading.
https://github.com/wiimdy/infrared_
What’s Infrared
Infrared is focused on building infrastructure around the Proof of Liquidity (PoL) mechanism pioneered by Berachain. The protocol aims to maximize value capture by providing easy-to-use liquid staking solutions for BGT and BERA, node infrastructure, and vaults. Through building solutions around Proof of Liquidity (PoL), Infrared is dedicated to enhancing the user experience and driving the growth of the Berachain ecosystem.
Why use Infrared?
Infrared simplifies participation in Berachain’s Proof of Liquidity (PoL), helping users maximize their BGT and BERA rewards through easy-to-use products like iBGT and iBERA. Infrared’s products also enable new flywheels throughout the Berachain ecosystem that wouldn’t otherwise be possible by making BGT and staked BERA composable.
PoL Related Function Summary
IBERA ←→ BeaconDeposit
How is deposit executed when a user mints IBERA?
asset: BERA, IBERA
1
2
3
4
5
6
7
8
9
10
11
12
13
function mint(address receiver) public payable returns (uint256 shares) {
// @dev make sure to compound yield earned from EL rewards first to avoid accounting errors.
compound();
// cache prior since updated in _deposit call
uint256 d = deposits;
uint256 ts = totalSupply();
// deposit bera request
uint256 amount = msg.value;
_deposit(amount);
...
}
- User executes
InfraredBERA.mint. - BERA is queued to
InfraredBERADepositor.queue(actual deposit is processed by an administrator, separating user and administrator functions). - IBERA is minted proportionally to
(totalSupply * amount) / deposits. - Keeper executes
InfraredBERADepositor.executeto deposit to BeaconDeposit with the corresponding Pubkey.
Infrared ←→ BaseBGT
How to use the baseBGT given by BlockrewardController?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function harvestBase(
address ibgt,
address bgt,
address ibera
) external returns (uint256 bgtAmt) {
// Since BGT balance has accrued to this contract, we check for what we've already accounted for
uint256 minted = IInfraredBGT(ibgt).totalSupply();
// The balance of BGT in the contract is the rewards accumilated from base rewards since the last harvest
// Since is paid out every block our validators propose (have a `Distributor::distibuteFor()` call)
uint256 balance = IBerachainBGT(bgt).balanceOf(address(this));
if (balance == 0) return 0;
// @dev the amount that will be minted is the difference between the balance accrued since the last harvestVault and the current balance in the contract.
// This difference should keep getting bigger as the contract accumilates more bgt from `Distributor::distibuteFor()` calls.
bgtAmt = balance - minted;
// @dev ensure that the `BGT::redeem` call won't revert if there is no BERA backing it.
// This is not unlikley since https://github.com/berachain/beacon-kit slots/blocks are not consistent there are times
// where the BGT rewards are not backed by BERA, in this case the BGT rewards are not redeemable.
// https://github.com/berachain/contracts-monorepo/blob/a28404635b5654b4de0627d9c0d1d8fced7b4339/src/pol/BGT.sol#L363
if (bgtAmt > bgt.balance) return 0;
// catch try can be used for additional security
try IBerachainBGT(bgt).redeem(IInfraredBERA(ibera).receivor(), bgtAmt) {
return bgtAmt;
} catch {
return 0;
}
}
- BGT in Infrared is rewardBGT + baseBGT. Of this, IBGT is minted for the amount of rewardBGT received, so baseBGT = totalBGT - total ibgt. (Where does it go from where?)
- Executing
harvestBaseredeems BaseBGT for BERA. - This BERA is sent to IBERAFeeReceivor.
InfraredVault ←→ Reward Vault
How is the Reward BGT distributed by Reward Vault to users?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function harvestVault(
RewardsStorage storage $,
IInfraredVault vault,
address bgt,
address ibgt,
address voter,
address ir,
uint256 rewardsDuration
) external returns (uint256 bgtAmt) {
// Ensure the vault is valid
if (vault == IInfraredVault(address(0))) {
revert Errors.VaultNotSupported();
}
// Record the BGT balance before claiming rewards since there could be base rewards that are in the balance.
uint256 balanceBefore = IBerachainBGT(bgt).balanceOf(address(this));
// Get the underlying Berachain RewardVault and claim the BGT rewards.
IBerachainRewardsVault rewardsVault = vault.rewardsVault();
rewardsVault.getReward(address(vault), address(this));
// Calculate the amount of BGT rewards received
bgtAmt = IBerachainBGT(bgt).balanceOf(address(this)) - balanceBefore;
// If no BGT rewards were received, exit early
if (bgtAmt == 0) return bgtAmt;
// Mint InfraredBGT tokens equivalent to the BGT rewards
IInfraredBGT(ibgt).mint(address(this), bgtAmt);
// Calculate the voter and protocol fees to charge on the rewards
(
uint256 _amt,
uint256 _amtVoter,
uint256 _amtProtocol
) = chargedFeesOnRewards(
bgtAmt,
$.fees[uint256(ConfigTypes.FeeType.HarvestVaultFeeRate)],
$.fees[uint256(ConfigTypes.FeeType.HarvestVaultProtocolRate)]
);
// Distribute the fees on the rewards.
_distributeFeesOnRewards(
$.protocolFeeAmounts,
voter,
ibgt,
_amtVoter,
_amtProtocol
);
// Send the post-fee rewards to the vault
if (_amt > 0) {
ERC20(ibgt).safeApprove(address(vault), _amt);
vault.notifyRewardAmount(ibgt, _amt);
}
// If IR token is set and mint rate is greater than zero, handle IR rewards.
uint256 mintRate = $.irMintRate;
if (ir != address(0) && mintRate > 0) {
// Calculate the amount of IR tokens to mint = BGT rewards * mint rate
uint256 irAmt = (bgtAmt * mintRate) / UNIT_DENOMINATOR;
if (!IInfraredGovernanceToken(ir).paused()) {
IInfraredGovernanceToken(ir).mint(address(this), irAmt);
{
// Check if IR is already a reward token in the vault
(, uint256 IRRewardsDuration, , , , , ) = vault.rewardData(
ir
);
if (IRRewardsDuration == 0) {
// Add IR as a reward token if not already added
vault.addReward(ir, rewardsDuration);
}
}
// Send the remaining IR rewards to the vault
if (irAmt > 0) {
ERC20(ir).safeApprove(address(vault), irAmt);
vault.notifyRewardAmount(ir, irAmt);
}
} else {
// @dev Misconfigured Role or Hit Supply Cap
emit ErrorMisconfiguredIRMinting(irAmt);
}
}
}
asset: lp token, reward bgt
LP stake
- User deposits LP by executing
InfraredVault.stake. - This is
approved by InfraredVault and thenstakein RewardVault.
Receiving LP stake rewards
- User executes
onRewardviaInfraredVault.getReward. -
Infrared.harvestVault→RewardLib.harvestVaultis executed.- BGT is received as a reward by Infrared through
rewardsVault.getReward. - IBGT is minted for the amount of BGT received and
addRewarded to InfraredVault.
- BGT is received as a reward by Infrared through
- Returns to
MultiReward.getRewardForUserand provides IBGT만큼 stored in rewards[_user][_rewardsToken].
LP withdraw
- User requests LP withdrawal via
InfraredVault.withdraw. - LP is withdrawn with
rewardVault.withdrawand thentransferred to the user.
Infrared ←→ BGTIncentiveDistributor…
How is the incentive taken by the operator (Infrared) used?
asset: incentive token, WBERA, BERA
- Keeper receives the incentive given by rewardVault via
Infrared.claimBGTIncentives. - Anyone can execute
Infrared.harvestBribesto move the incentive in Infrared to BribeCollector (the token must be whitelisted at this time). - Anyone can exchange WBERA and incentive token via
BribeCollector.claimFees. - Infrared reclaims the WBERA in BribeCollector via
collectBribesand distributes it to BERAfeeReceivor and IBGT Vault viacollectBribesInWBERA.
Bonus: What does IBERAfeeReceivor do? Why does it keep receiving BERA?
- Source of BERA: Protocol incentive, base BGT, i.e., most of the rewards received by the operator.
-
sweep()is executed frequently externally. This consolidates the accumulated BERA, setting aside 10% of the total as operator rewards and putting the rest into_deposit(). - Accumulated operator rewards are distributed via
RewardLib.harvestOperatorRewards.
Infrared ←→ Validator Operator
- The operator selected by the validator uses Infrared’s contract. https://berascan.com/address/0xb71b3daea39012fb0f2b14d2a9c86da9292fc126 a41 is the operator.
- Various interactions here, such as chef settings, boosts, etc.
Lido diff
Infrared, broadly speaking, is a Liquid Staking solution, so I investigated its differences from Lido, the most famous one.
Lido stake logic
-
Lido.submit() https://github.com/lidofinance/core/blob/d186530e74e07569295ac5de399389e5438bf567/contracts/0.4.24/Lido.sol#L917-L947
When a user sends Ether via submit, it’s placed in a buffer, and deposits are made in multiples of 32 ETH. Therefore, stETH is minted to the user immediately.
-
Lido.deposit() https://github.com/lidofinance/core/blob/d186530e74e07569295ac5de399389e5438bf567/contracts/0.4.24/Lido.sol#L694-L723
Once a certain amount of ETH is accumulated, it checks if it’s possible and then forwards it to the stakingRouter.
-
StakingLouter.deposit() https://github.com/lidofinance/core/blob/d186530e74e07569295ac5de399389e5438bf567/contracts/0.8.9/StakingRouter.sol#L1251-L1289
It retrieves the stored withdrawal address and creates the publickey and signature based on the data.
-
BeaconChainDepositor.
_makeBeaconChainDeposits32ETH()https://github.com/lidofinance/core/blob/d186530e74e07569295ac5de399389e5438bf567/contracts/0.8.9/BeaconChainDepositor.sol#L41-L69It performs an argument length check (pubkey, signature) and passes it to the deposit contract.
-
DEPOSIT_CONTRACT.deposit() https://github.com/lidofinance/core/blob/d186530e74e07569295ac5de399389e5438bf567/contracts/0.6.11/deposit_contract.sol#L101-L159
The corresponding deposit contract: https://etherscan.io/address/0x00000000219ab540356cBB839Cbe05303d7705Fa#readContract
It verifies the arguments and deposit value, then adds a node to the tree based on pubkey, withdrawal, and amount.
Infrared stake logic
-
InfraredBERA.mint() https://github.com/wiimdy/infrared_/blob/265182e933452ec867565ffc56ef976a34fe4db3/src/staking/InfraredBERA.sol#L213-L232
The user executes this by sending BERA. After _deposit is executed, IBERA.mint is executed.
-
Calculates the total deposit and passes it to the depositor.
-
InfraredBERADepositor.queue() https://github.com/wiimdy/infrared_/blob/265182e933452ec867565ffc56ef976a34fe4db3/src/staking/InfraredBERADepositor.sol#L55-L69
Calculates the reserved amount and waits for execute to be called.
-
InfraredBERADepositor.execute() https://github.com/wiimdy/infrared_/blob/265182e933452ec867565ffc56ef976a34fe4db3/src/staking/InfraredBERADepositor.sol#L76-L159
-
Corresponding transaction log: https://app.blocksec.com/explorer/tx/berachain/0x26f147fa2d8ac7b3bb857f042a3106548f042024de401f2fbce5b892d04c29a3
-
Total execution log: https://dune.com/queries/5149238/8481973/
-
-
BeraDeposit.deposit() https://github.com/berachain/contracts/blob/b3da3d3452999975c8c93f07a97c7b107d18a6f4/src/pol/BeaconDeposit.sol#L84-L128
Deposit Contract: https://berascan.com/address/0x4242424242424242424242424242424242424242
After argument verification, it executes with _deposit().
-
Burns the received BERA to the 0 address.
Differences
- In Berachain, the entire deposited amount is sent to the 0 address, unlike Ethereum. The actual Ethereum deposit contract shows a large amount of Ether accumulated.
- In Ethereum, the Merkle root is recorded in the contract during deposit, whereas in Berachain, the Merkle root is managed via a beaconkit helper.
Similarities
- Both accumulate a certain amount, and then an authorized account executes the deposit. (Minimum deposit exists, gas saving)
- The user does not specify which validator to stake to (pubkey), so the protocol sets it. (In the case of Infrared, validators are divided by company. What is the criterion?)
- The logic flow (user stake, administrator deposit process) is similar.