-
Notifications
You must be signed in to change notification settings - Fork 8
Feature/GBI-2807 - PegIn contract #354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slither found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a new PegIn contract to the LBC split architecture, including comprehensive unit tests and documentation. The implementation moves the punisher reward mechanism from the PegIn contract to the CollateralManagement contract, where rewards are calculated from penalties taken from provider collaterals.
- Add complete PegIn contract implementation with deposit, withdraw, callForUser, and registerPegIn functionality
- Integrate punisher reward system into CollateralManagement contract
- Add comprehensive test suite covering all PegIn contract functions and edge cases
Reviewed Changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/utils/quotes.ts | Add reward calculation utility and update PegIn quote type definition |
| test/utils/fixtures.ts | Move collateral management deployment to shared fixtures |
| test/utils/constants.ts | Add PegIn-specific constants and state enums |
| test/pegout/*.test.ts | Update tests to use new reward calculation and collateral management interface |
| test/pegin/*.test.ts | Add comprehensive test suite for all PegIn contract functions |
| contracts/test-contracts/*.sol | Add mock contracts for testing withdrawal, reentrancy, and bridge scenarios |
| contracts/split/*.sol | Update CollateralManagement to include punisher reward mechanism |
| contracts/libraries/Flyover.sol | Add common error definitions used across contracts |
| contracts/interfaces/*.sol | Add IPegIn interface and update ICollateralManagement with reward parameters |
| contracts/PegInContract.sol | Add complete PegIn contract implementation |
| contracts/PegOutContract.sol | Update to use shared error definitions and reward system |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| function withdraw(uint256 amount) external nonReentrant override { | ||
| uint256 balance = _balances[msg.sender]; | ||
| if (balance < amount) { | ||
| revert Flyover.NoBalance(amount, balance); | ||
| } | ||
| _decreaseBalance(msg.sender, amount); | ||
| emit Withdrawal(msg.sender, amount); | ||
| (bool success, bytes memory reason) = msg.sender.call{value: amount}(""); | ||
| if (!success) { | ||
| revert Flyover.PaymentFailed(msg.sender, amount, reason); | ||
| } | ||
| } |
Check warning
Code scanning / Slither
Low-level calls Warning
- (success,reason) = msg.sender.call{value: amount}()
| function callForUser( | ||
| Quotes.PegInQuote calldata quote | ||
| ) external payable nonReentrant override returns (bool) { | ||
| if(!_collateralManagement.isRegistered(_PEG_TYPE, msg.sender)) { | ||
| revert Flyover.ProviderNotRegistered(msg.sender); | ||
| } | ||
| if (quote.liquidityProviderRskAddress != msg.sender) { | ||
| revert Flyover.InvalidSender(quote.liquidityProviderRskAddress, msg.sender); | ||
| } | ||
| uint256 newBalance = _balances[quote.liquidityProviderRskAddress] + msg.value; | ||
| if (newBalance < quote.value) { | ||
| revert Flyover.InsufficientAmount(newBalance, quote.value); | ||
| } | ||
|
|
||
| bytes32 quoteHash = _hashPegInQuote(quote); | ||
| if (_processedQuotes[quoteHash] != PegInStates.UNPROCESSED_QUOTE) { | ||
| revert QuoteAlreadyProcessed(quoteHash); | ||
| } | ||
|
|
||
| _increaseBalance(quote.liquidityProviderRskAddress, msg.value); | ||
|
|
||
| // This check ensures that the call cannot be performed with less gas than the agreed amount | ||
| if (gasleft() < quote.gasLimit + _MAX_CALL_GAS_COST) { | ||
| revert InsufficientGas(gasleft(), quote.gasLimit + _MAX_CALL_GAS_COST); | ||
| } | ||
| _callRegistry[quoteHash].timestamp = block.timestamp; | ||
| _processedQuotes[quoteHash] = PegInStates.CALL_DONE; | ||
|
|
||
| (bool success,) = quote.contractAddress.call{ | ||
| gas: quote.gasLimit, | ||
| value: quote.value | ||
| }(quote.data); | ||
|
|
||
| if (success) { | ||
| _callRegistry[quoteHash].success = true; | ||
| _decreaseBalance(quote.liquidityProviderRskAddress, quote.value); | ||
| } | ||
|
|
||
| emit CallForUser( | ||
| msg.sender, | ||
| quote.contractAddress, | ||
| quoteHash, | ||
| quote.gasLimit, | ||
| quote.value, | ||
| quote.data, | ||
| success | ||
| ); | ||
| return success; | ||
| } |
Check failure
Code scanning / Slither
Reentrancy vulnerabilities High
External calls:
- (success,None) = quote.contractAddress.call{gas: quote.gasLimit,value: quote.value}(quote.data)
State variables written after the call(s):
- _decreaseBalance(quote.liquidityProviderRskAddress,quote.value)
- _balances[dest] -= amount
PegInContract._balances can be used in cross function reentrancies:
- PegInContract.getBalance(address)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please evaluate the usage of Checks-Effects-Interactions pattern here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hakob made a similar comment related to the reentrancy alert, I explained the rationale of doing it in this way in my response, please take a look a let me know your thoughts about the need of keeping track of the result of the call.
The response -> #354 (comment)
| function callForUser( | ||
| Quotes.PegInQuote calldata quote | ||
| ) external payable nonReentrant override returns (bool) { | ||
| if(!_collateralManagement.isRegistered(_PEG_TYPE, msg.sender)) { | ||
| revert Flyover.ProviderNotRegistered(msg.sender); | ||
| } | ||
| if (quote.liquidityProviderRskAddress != msg.sender) { | ||
| revert Flyover.InvalidSender(quote.liquidityProviderRskAddress, msg.sender); | ||
| } | ||
| uint256 newBalance = _balances[quote.liquidityProviderRskAddress] + msg.value; | ||
| if (newBalance < quote.value) { | ||
| revert Flyover.InsufficientAmount(newBalance, quote.value); | ||
| } | ||
|
|
||
| bytes32 quoteHash = _hashPegInQuote(quote); | ||
| if (_processedQuotes[quoteHash] != PegInStates.UNPROCESSED_QUOTE) { | ||
| revert QuoteAlreadyProcessed(quoteHash); | ||
| } | ||
|
|
||
| _increaseBalance(quote.liquidityProviderRskAddress, msg.value); | ||
|
|
||
| // This check ensures that the call cannot be performed with less gas than the agreed amount | ||
| if (gasleft() < quote.gasLimit + _MAX_CALL_GAS_COST) { | ||
| revert InsufficientGas(gasleft(), quote.gasLimit + _MAX_CALL_GAS_COST); | ||
| } | ||
| _callRegistry[quoteHash].timestamp = block.timestamp; | ||
| _processedQuotes[quoteHash] = PegInStates.CALL_DONE; | ||
|
|
||
| (bool success,) = quote.contractAddress.call{ | ||
| gas: quote.gasLimit, | ||
| value: quote.value | ||
| }(quote.data); | ||
|
|
||
| if (success) { | ||
| _callRegistry[quoteHash].success = true; | ||
| _decreaseBalance(quote.liquidityProviderRskAddress, quote.value); | ||
| } | ||
|
|
||
| emit CallForUser( | ||
| msg.sender, | ||
| quote.contractAddress, | ||
| quoteHash, | ||
| quote.gasLimit, | ||
| quote.value, | ||
| quote.data, | ||
| success | ||
| ); | ||
| return success; | ||
| } |
Check notice
Code scanning / Slither
Return Bomb Low
(success,None) = quote.contractAddress.call{gas: quote.gasLimit,value: quote.value}(quote.data)
| function callForUser( | ||
| Quotes.PegInQuote calldata quote | ||
| ) external payable nonReentrant override returns (bool) { | ||
| if(!_collateralManagement.isRegistered(_PEG_TYPE, msg.sender)) { | ||
| revert Flyover.ProviderNotRegistered(msg.sender); | ||
| } | ||
| if (quote.liquidityProviderRskAddress != msg.sender) { | ||
| revert Flyover.InvalidSender(quote.liquidityProviderRskAddress, msg.sender); | ||
| } | ||
| uint256 newBalance = _balances[quote.liquidityProviderRskAddress] + msg.value; | ||
| if (newBalance < quote.value) { | ||
| revert Flyover.InsufficientAmount(newBalance, quote.value); | ||
| } | ||
|
|
||
| bytes32 quoteHash = _hashPegInQuote(quote); | ||
| if (_processedQuotes[quoteHash] != PegInStates.UNPROCESSED_QUOTE) { | ||
| revert QuoteAlreadyProcessed(quoteHash); | ||
| } | ||
|
|
||
| _increaseBalance(quote.liquidityProviderRskAddress, msg.value); | ||
|
|
||
| // This check ensures that the call cannot be performed with less gas than the agreed amount | ||
| if (gasleft() < quote.gasLimit + _MAX_CALL_GAS_COST) { | ||
| revert InsufficientGas(gasleft(), quote.gasLimit + _MAX_CALL_GAS_COST); | ||
| } | ||
| _callRegistry[quoteHash].timestamp = block.timestamp; | ||
| _processedQuotes[quoteHash] = PegInStates.CALL_DONE; | ||
|
|
||
| (bool success,) = quote.contractAddress.call{ | ||
| gas: quote.gasLimit, | ||
| value: quote.value | ||
| }(quote.data); | ||
|
|
||
| if (success) { | ||
| _callRegistry[quoteHash].success = true; | ||
| _decreaseBalance(quote.liquidityProviderRskAddress, quote.value); | ||
| } | ||
|
|
||
| emit CallForUser( | ||
| msg.sender, | ||
| quote.contractAddress, | ||
| quoteHash, | ||
| quote.gasLimit, | ||
| quote.value, | ||
| quote.data, | ||
| success | ||
| ); | ||
| return success; | ||
| } |
Check warning
Code scanning / Slither
Low-level calls Warning
| function registerPegIn( | ||
| Quotes.PegInQuote calldata quote, | ||
| bytes calldata signature, | ||
| bytes calldata btcRawTransaction, | ||
| bytes calldata partialMerkleTree, | ||
| uint256 height | ||
| ) external nonReentrant override returns (int256) { | ||
| bytes32 quoteHash = _hashPegInQuote(quote); | ||
| _validateRegisterParams(quote, quoteHash, height, signature); | ||
| int256 registerResult = _registerBridge(quote, btcRawTransaction, partialMerkleTree, height, quoteHash); | ||
|
|
||
| bool btcRefunded = registerResult == _BRIDGE_REFUNDED_USER_ERROR_CODE || | ||
| registerResult == _BRIDGE_REFUNDED_LP_ERROR_CODE; | ||
| if (registerResult == _BRIDGE_UNPROCESSABLE_TX_VALIDATIONS_ERROR) { | ||
| revert NotEnoughConfirmations(); | ||
| } else if (!btcRefunded && registerResult < 1) { | ||
| revert UnexpectedBridgeError(registerResult); | ||
| } | ||
|
|
||
| Registry memory callRegistry = _callRegistry[quoteHash]; | ||
| delete _callRegistry[quoteHash]; | ||
| if (_shouldPenalize(quote, registerResult, callRegistry.timestamp, height)) { | ||
| _collateralManagement.slashPegInCollateral(msg.sender, quote, quoteHash); | ||
| } | ||
| if (btcRefunded) { | ||
| _processedQuotes[quoteHash] = PegInStates.PROCESSED_QUOTE; | ||
| emit BridgeCapExceeded(quoteHash, registerResult); | ||
| return registerResult; | ||
| } | ||
|
|
||
| // the amount is safely assumed positive because it's already been validated there's | ||
| // no (negative) error code being returned by the bridge. | ||
| uint256 transferredAmount = uint256(registerResult); | ||
| Quotes.checkAgreedAmount(quote, transferredAmount); | ||
|
|
||
| _processedQuotes[quoteHash] = PegInStates.PROCESSED_QUOTE; | ||
| emit PegInRegistered(quoteHash, transferredAmount); | ||
| if (callRegistry.timestamp > 0) { | ||
| _registerCallDone(quote, quoteHash, callRegistry.success, transferredAmount); | ||
| } else { | ||
| _registerCallNotDone(quote, quoteHash, transferredAmount); | ||
| } | ||
| return registerResult; | ||
| } |
Check warning
Code scanning / Slither
Reentrancy vulnerabilities Medium
External calls:
- registerResult = _registerBridge(quote,btcRawTransaction,partialMerkleTree,height,quoteHash)
- _bridge.registerFastBridgeBtcTransaction(btcRawTransaction,height,partialMerkleTree,derivationHash,quote.btcRefundAddress,address(this),quote.liquidityProviderBtcAddress,callRegistry.timestamp > 0 && callRegistry.success)
- _collateralManagement.slashPegInCollateral(msg.sender,quote,quoteHash)
State variables written after the call(s):
- _processedQuotes[quoteHash] = PegInStates.PROCESSED_QUOTE
PegInContract._processedQuotes can be used in cross function reentrancies:
- PegInContract.getQuoteStatus(bytes32)
- _processedQuotes[quoteHash] = PegInStates.PROCESSED_QUOTE
PegInContract._processedQuotes can be used in cross function reentrancies:
- PegInContract.getQuoteStatus(bytes32)
alexjavabraz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Evaluate the usage of Checks-Effects-Interactions pattern
hakobRsk
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason, the getMinPegin is not implemented(it is mentioned in the docs but not present in the legacy contract)?
And I assume getRewardPercentage and rewardP is not implemented here as they are the part of the punishing mechanism, right?
Otherwise, LGTM
| _callRegistry[quoteHash].timestamp = block.timestamp; | ||
| _processedQuotes[quoteHash] = PegInStates.CALL_DONE; | ||
|
|
||
| (bool success,) = quote.contractAddress.call{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Currently we don't have a behavior to revert if the call(in any way) returns
false. May be we should handle that case inside the contract as well? - This part is also flagged as reentrancy vulnerability. If we agree to add a revert mechanism as mentioned in the first point, then maybe we could do smth like this?
_callRegistry[quoteHash].success = true;
_decreaseBalance(quote.liquidityProviderRskAddress, quote.value);
(bool success,) = quote.contractAddress.call{
gas: quote.gasLimit,
value: quote.value
}(quote.data);
if(!success) revert();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both questions are related, the context here is that we need to keep track of the result of the call to the contractAddress, as it affects the flow in the following ways:
- If the LP did make the call, but the call wasn't successful, then they aren't applicable for penalization (assuming timing was ok) and they are still refunded with the
gasFeeandcallFee, but not the value as the call reverted - If the LP did make the call and was successful, then they aren't applicable for penalization (assuming timing was ok) and they are refunded with
gasFee,callFeeandvalueamounts - If the LP didn't make the call then they are applicable for penalization (assuming timing was ok) but in that case the
callForUsernever executes
The point is that if we don't keep track of the result of the call in the state of the contract then we won't be to determine most of the scenarios inside the registerPegIn. This only happens with the CallForUser event, so if you see something similar in other part of the contract then we should change that but in this particular case I think we are pretty much forced to maintain the modification of the state after the call. Anyway if you have any proposal to rewrite the function but maintaining the overall logic we can try with that, perhaps we could add
_callRegistry[quoteHash].success = true;
_decreaseBalance(quote.liquidityProviderRskAddress, quote.value);
optimistically before the external call, but we'd still need to emit the event with the result of the call and rollback that modification in case the call fails, so we would be in a similar situation.
Regarding the reentrancy alert, yes, even though I included some tests specifically to ensure this shouldn't be affected by that vulnerability, slither seems not to be detecting the nonReentrant modifier. I already raised this topic with security team and I'm waiting for their response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, thanks for the clarification. I think we can keep it this way then, especially if there is an explicit reentrancy guard. We could still try to follow the DEI pattern, but that would unnecessarily add the gas cost for the caller.
@hakobRsk There is no reason, I forgot to add that getter, thanks for noticing. Regarding |
alexjavabraz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
What
Why
To continue with the LBC split, the remaining contracts are
CollateralManagementandFlyoverDiscoveryTask
https://rsklabs.atlassian.net/browse/GBI-2807
Important
The punisher reward mechanism was moved to
CollateralManagementinstead of thePegInContract. The reason of this is because the tokens used to reward the punisher (whomever calls theregisterPegInto penalize a liquidity provider) are taken from the provider's collateral, so it makes more sense to have that mechanism inCollateralManagement. The notion page will be updated with this change.