The stROSEMinter contract is designed to handle delegation and undelegation of tokens to validators on the Oasis consensus layer. It uses a system of shares to represent the amount of tokens staked with validators. The core functionalities include delegating tokens to validators, receiving shares in return, and undelegating shares to retrieve tokens.
Relevant Components:
Delegation structure: Represents the shares held by a user.
DelegationReceipt and UndelegationReceipt structures: Track the details of delegation and undelegation transactions.
delegate(), takeReceiptDelegate(), undelegate(), and takeReceiptUndelegate() functions: Handle the delegation and undelegation processes.
Issue:
The contract assumes a constant relationship between shares and tokens, which is not accurate in proof-of-stake systems where validators can be slashed. Slashing reduces the actual value of shares, but the contract does not reflect this change, leading to incorrect share accounting.
Detailed Examination:
Delegation:
The delegate() function correctly records the amount delegated and issues a receipt.
The takeReceiptDelegate() function updates the shares without considering potential slashing events. It assumes that the shares received are always equivalent to the amount delegated, which is not true if slashing occurs.
function takeReceiptDelegate(uint64 receiptId) public onlyOwner returns (uint128 shares) {
DelegationReceipt storage receipt = delegationReceipts[receiptId];
require(block.number > receipt.blockNumber, "ReceiptNotReady");
require(receipt.exists, "ReceiptNotExists");
require(receipt.receiptTaken == false, "ReceiptAlreadyTaken");
shares = Subcall.consensusTakeReceiptDelegate(receiptId);
Delegation storage d = delegations[receipt.to];
// update receipt with the necessary info
receipt.shares = shares;
receipt.receiptTaken = true;
receipt.receiptTakenBlockNumber = block.number;
// update delegation amount and shares
d.shares += shares;
_addDelegation(receipt.to);
emit TakeReceiptDelegate(receiptId);
}
Undelegation:
The undelegate() function allows undelegation based on the number of shares without considering the actual value of those shares post-slashing. This can lead to a situation where the contract tries to withdraw more tokens than it actually has.
User A delegates 1000 ROSE and receives 1000 shares.
Validator is slashed, reducing the value of shares by 10%.
User A's 1000 shares now represent only 900 ROSE.
User B delegates 900 ROSE and receives 1000 shares (due to reduced share value).
The contract shows both User A and User B having 1000 shares, but they represent different amounts of ROSE.
If both users try to undelegate all their shares, the contract will attempt to withdraw 2000 ROSE worth of shares, but only 1800 ROSE actually exist.
Impact
The contract fails to account for slashing events, leading to incorrect share accounting and potential financial discrepancies. This can result in incorrect representation of user stakes, potential loss of funds for users, contract insolvency if multiple users undelegate after slashing events, and unfair distribution of rewards and losses among users.
Attack Scenario:
Owner calls delegate() to delegate 1000 ROSE to a validator.
Validator gets slashed, reducing the value of shares by 10%.
Owner calls delegate() again to delegate 900 ROSE to the same validator.
Owner calls takeReceiptDelegate() to update the shares for both delegations.
Owner calls undelegate() to undelegate all shares.
The contract attempts to withdraw more tokens than it actually has, leading to financial discrepancies.
Proof of Concept
Initial Delegation:
User A delegates 1000 ROSE and receives 1000 shares.
Code Reference: delegate()
Slashing Event:
Validator is slashed, reducing the value of shares by 10%.
No direct code reference, but this is an external event affecting the share value.
Subsequent Delegation:
User B delegates 900 ROSE and receives 1000 shares (due to reduced share value).
Code Reference: delegate()
Receipt Handling:
User A and User B call takeReceiptDelegate() to update their shares.
Code Reference: takeReceiptDelegate()
Undelegation:
Both users call undelegate() to undelegate all their shares.
Code Reference: undelegate()
Financial Discrepancy:
The contract attempts to withdraw 2000 ROSE worth of shares, but only 1800 ROSE actually exist.
Github username: @4gontuk Twitter username: 4gontuk Submission hash (on-chain): 0xd1b823fbb4f9e92f8d92a6878d28203ea5725679b5ceec64edb4aa7e3136f0fb Severity: low
Description:
Description:
The
stROSEMinter
contract is designed to handle delegation and undelegation of tokens to validators on the Oasis consensus layer. It uses a system of shares to represent the amount of tokens staked with validators. The core functionalities include delegating tokens to validators, receiving shares in return, and undelegating shares to retrieve tokens.Relevant Components:
Delegation
structure: Represents the shares held by a user.DelegationReceipt
andUndelegationReceipt
structures: Track the details of delegation and undelegation transactions.delegate()
,takeReceiptDelegate()
,undelegate()
, andtakeReceiptUndelegate()
functions: Handle the delegation and undelegation processes.Issue:
The contract assumes a constant relationship between shares and tokens, which is not accurate in proof-of-stake systems where validators can be slashed. Slashing reduces the actual value of shares, but the contract does not reflect this change, leading to incorrect share accounting.
Detailed Examination:
Delegation:
delegate()
function correctly records the amount delegated and issues a receipt.takeReceiptDelegate()
function updates the shares without considering potential slashing events. It assumes that the shares received are always equivalent to the amount delegated, which is not true if slashing occurs.Undelegation:
undelegate()
function allows undelegation based on the number of shares without considering the actual value of those shares post-slashing. This can lead to a situation where the contract tries to withdraw more tokens than it actually has.Highest Impact Scenario:
Impact
The contract fails to account for slashing events, leading to incorrect share accounting and potential financial discrepancies. This can result in incorrect representation of user stakes, potential loss of funds for users, contract insolvency if multiple users undelegate after slashing events, and unfair distribution of rewards and losses among users.
Attack Scenario:
delegate()
to delegate 1000 ROSE to a validator.delegate()
again to delegate 900 ROSE to the same validator.takeReceiptDelegate()
to update the shares for both delegations.undelegate()
to undelegate all shares.Proof of Concept
Initial Delegation:
delegate()
Slashing Event:
Subsequent Delegation:
delegate()
Receipt Handling:
takeReceiptDelegate()
to update their shares.takeReceiptDelegate()
Undelegation:
undelegate()
to undelegate all their shares.undelegate()
Financial Discrepancy:
undelegate()