2021-04-marginswap-findings's People
2021-04-marginswap-findings's Issues
Events not indexed
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The CrossDeposit
, CrossTrade
, CrossWithdraw
, CrossBorrow
, CrossOvercollateralizedBorrow
events in MarginRouter
are not indexed.
Impact
Off-chain scripts cannot efficiently filter these events.
Recommended mitigation steps
Add an index on important arguments like trader
.
`getReserves` does not check if tokens match
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The UniswapStyleLib.getReserves
function does not check if the tokens are the pair's underlying tokens.
It blindly assumes that the tokens are in the wrong order if the first one does not match but they could also be completely different tokens.
Impact
It could be the case that output amounts are computed for completely different tokens because a wrong pair was provided.
diffMaxMinRuntime gets default value of 0
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
uint256 public diffMaxMinRuntime;
This variable is never set nor updated so it gets a default value of 0.
Impact
diffMaxMinRuntime with 0 value is making the calculations that use it either always return 0 (when multiplying) or fail (when dividing) when calculating bucket indexes or sizes.
Recommended mitigation steps
Set the appropriate value for diffMaxMinRuntime and update it whenever min or max runtime variables change.
setLeveragePercent should check that new _leveragePercent >= 100
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
function setLeveragePercent should check that the _leveragePercent >= 100 so that this calculation will not fail later:
(leveragePercent - 100)
Impact
This variable can only be set by admin so as long as he sets the appropriate value it should be fine.
Recommended mitigation steps
It is always nice to enforce such things via code. Code is law they say.
Function parameter named timestamp
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
This is a minor suggestion.
The function viewCumulativeYieldFP in HourlyBondSubscrptionLending.sol has a parameter named timestamp.
Impact
As there is also an inbuilt variable block.timestamp this could be confusing.
Recommended mitigation steps
Rename the parameter timestamp to a slightly different name.
Missing checks if pairs equal tokens
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The UniswapStyleLib.getAmountsOut
, PriceAware.setLiquidationPath
(and others) don't check that path.length + 1 == tokens.length
which should always hold true.
Also, it does not check that the tokens actually match the pair.
Impact
It's easy to set faulty liquidation paths which then end up reverting the liquidation transactions.
Recommended mitigation steps
Add the missing checks.
runtime > 1 hours error message discrepancy
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
Here, the revert message says that the value needs to be at least 1 hour, however, the code allows value only above the 1 hour (> instead of >=):
require(runtime > 1 hours, "Min runtime needs to be at least 1 hour");
Impact
no impact on security, just a discrepancy between the check and message.
Recommended mitigation steps
Replace > with >= or update the error message to reflect that.
An erroneous constructor's argument could block the withdrawReward
Email address
Handle
s1m0
Eth address
0x9b3E9e3E4a174d59279FC7cd268e035992412384
Vulnerability details
The constructor of IncentiveDistribution https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L32
take as argument the address of MFI token but it doesn't check that is != address(0).
Not worth an issue alone but IncentiveDistribution imports IERC20.sol and it never use it.
Impact
In case the address(0) is passed as arguement the withdrawReward woul fail https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L261 and due to the fact that
MFI is immutable the only solution would be to redeploy the contract meanwhile losing trust from the users.
Proof of concept
Deploy IncentiveDistribution with 0 as _MFI argument and then call withdrawReward.
Tools used
Manual analysis
Recommended mitigation steps
Check _MFI != address(0)
Rewards cannot be withdrawn
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The rewards for a recipient in IncentiveDistribution.sol
are stored in the storage mapping indexed by recipient accruedReward[recipient]
and the recipient is the actual margin trader account, see updateAccruedReward
.
These rewards are supposed to be withdrawn through the withdrawReward
function but msg.sender
is used here instead of a recipient
(withdrawer
) parameter.
However, msg.sender
is enforced to be the incentive reporter and can therefore not be the margin trader.
Impact
Nobody can withdraw the rewards.
Recommended mitigation steps
Remove the isIncentiveReporter(msg.sender)
check from withdrawReward
function.
Liquidations can be sandwich attacked
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The liquidation functions liquidateToPeg/liquidateFromPeg
uses a minReturn
value of zero which allows infinite slippage.
An attacker can frontrun a liquidation trade by buying up the same asset, driving the price higher and resulting in the liquidator receiving fewer tokens. The attacker then backruns the trade by selling the tokens received by their first trade again for a profit. (sandwich attack)
Impact
Liquidators earn less profit
Recommended mitigation steps
Let liquidators define minReturn
amounts.
[INFO] Consistent function names
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
In contract IsolatedMarginTrading the function to set leverage is named "setLeveragePercent" and in CrossMarginTrading function that does the same is named "setLeverage". It would be better to unify them and give the same names to make it more consistent.
[INFO] Variable is declared and initialized with different values
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
It is strange to see a variable assigned a value in the declaration but immediadetely overriden in the constructor:
uint256 public maintenanceStakePerBlock = 10 ether;
constructor(
...
maintenanceStakePerBlock = 1 ether;
PriceAware uses prices from getAmountsOut
Vulnerability details
getPriceFromAMM relies on values returned from getAmountsOut which can be manipulated (e.g. with the large capital or the help of flash loans). The impact is reduced with UPDATE_MIN_PEG_AMOUNT and UPDATE_MAX_PEG_AMOUNT, however, it is not entirely eliminated.
Impact
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Recommended mitigation steps
Uniswap v2 recommends using their TWAP oracle: https://uniswap.org/docs/v2/core-concepts/oracles/
sortTokens can be simplified
Vulnerability details
this is a minor suggestion:
The function sortTokens UniswapStyleLib.sol returns 2 values, but only the first return value is used:
MarginRouter.sol: (address token0, ) = UniswapStyleLib.sortTokens...
UniswapStyleLib.sol: (address token0, ) = sortTokens..
In both cases the used return value is compared to the first parameter of the function call.
Conclusion: the function is only used to determine the smaller of the two tokens, not really to sort tokens.
Handle
gpersoon
Email address
Eth address
gpersoon.eth
Impact
The code is somewhat more difficult to read and a bit longer than neccesary.
Recommended mitigation steps
simplify the code:
function ASmallerThanB(address tokenA, address tokenB)
internal
pure
returns (bool)
{
require(tokenA != tokenB, "Identical address!");
require(tokenA != address(0), "Zero address!");
require(tokenB != address(0), "Zero address!");
return tokenA < tokenB;
}
Wrong liquidation logic
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The belowMaintenanceThreshold
function decides if a trader can be liquidated:
function belowMaintenanceThreshold(CrossMarginAccount storage account)
internal
returns (bool)
{
uint256 loan = loanInPeg(account, true);
uint256 holdings = holdingsInPeg(account, true);
// The following should hold:
// holdings / loan >= 1.1
// =>
return 100 * holdings >= liquidationThresholdPercent * loan;
}
The inequality in the last equation is wrong because it says the higher the holdings (margin + loan) compared to the loan, the higher the chance of being liquidated.
The inverse equality was probably intended return 100 * holdings <= liquidationThresholdPercent * loan;
.
Impact
Users that shouldn't be liquidated can be liquidated, and users that should be liquidated cannot get liquidated.
Recommended mitigation steps
Fix the equation.
Email address
Handle
@cmichelio
The bug submissions are accessible
Email address
Handle
gpersoon
Eth address
0x8e2A89fF2F45ed7f8C8506f846200D671e2f176f
Vulnerability details
The github token can be retrieved from the https://c4-marginswap.netlify.app/ website
Proof of concept
In the source of the website (createIssue.js) you can see github is directly accessed from the website.
With Fiddler you can search for GITHUB and see:
REACT_APP_MAILGUN_DOMAIN:"mg.code423n4.com",
REACT_APP_MAILGUN_KEY:"a9763c0878cd90413bf11615456692e7-b6d086a8-be1c8bad",
REACT_APP_GITHUB_TOKEN:"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"
With Fiddler you can search for code-423n4 and see:
"owner":"code-423n4",
"repo":"marginswap-results"
With the GITHUB token you can retrieve the issues:
curl -H "Authorization: token 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" https://api.github.com/repos/code-423n4/marginswap-results/issues
This shows:
"body": "# Email address\n\[email protected]\n\n\n# Handle\n\nadamavenir\n\n\n# Eth address\n\n123123123\n\n\n# Vulnerability details\n\nSome details:\n\n\ndetails(schemtails)\n
\n\n\n# Impact\n\nBrace for it!\n\n\n# Proof of concept\n\n- proof\n- of \n- concept\n\n\n# Tools used\n\nI used no tools except this form and my BARE HANDS!\n\n\n# Recommended mitigation steps\n\nI would recommend not doing this bug.\n\n",
Impact
With the token you can access the submissions of others and share in their prices.
Tools used
Fiddler (https://www.telerik.com/)
And the developer console of Chrome to look at the source.
Recommended mitigation steps
Either open source the bug submission enterly
Or split the bug submission application in two parts, where only a backend has the Github keys and does the creating of the issues.
There are tools that promise to do this, i haven't looked into them:
https://fire.fundersclub.com/
https://zapier.com/apps/github/integrations/gmail/10314/create-github-issues-from-new-emails-on-gmail-business-gmail-accounts-only
https://flow.microsoft.com/en-us/galleries/public/templates/6b590f10bc9011e6b2e2c98b01575bae/send-an-email-to-create-github-issues/
No entry checks in crossSwap[Exact]TokensFor[Exact]Tokens
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
The functions crossSwapTokensForExactTokens and crossSwapExactTokensForTokens of MarginRouter.sol do not check who is calling the function.
They also do not check the contents of pairs and tokens
They also do not check if the size of pairs and tokens is the same
registerTradeAndBorrow within registerTrade does seem to do an entry check
(require(isMarginTrader(msg.sender)...)
however as this is an external function msg.sender is the address of MarginRouter.sol, which will verify ok.
Impact
Calling these functions allow the caller to trade on behalf of marginswap, which could result in losing funds.
It's possible to construct all parameters to circumvent the checks.
Also the "pairs" can be fully specified; they are contract addresses that are called from getAmountsIn / getAmountsOut and from pair.swap.
This way you can call arbitrary (self constructed) code, which can reentrantly call the marginswap code.
Proof of concept
Based on source code review.
A real attack requires the deployed code to be able to construct the right values.
Tools used
remix
Recommended mitigation steps
Limit who can call the functions
Perhaps whitelist contents of pairs and tokens
Check the size of pairs and tokens is the same
Naming convention for internal functions not used consistently
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
This is a minor suggestion.
Most internal function names start with an underscore (_)
However quite a lot of internal function names don't follow this convention.
Proof of concept
One example is: updateHourlyBondAmount in HourlyBondSubscriptionLending.sol
Also all the functions in RoleAware.sol don't comply to the standard.
Impact
The code is more difficult to read if a naming convention is not used consistently.
Recommended mitigation steps
Add an underscore (_) prefix to all internal functions.
Role 9 in Roles.sol
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
This is a minor suggestion.
Roles.sol contains the following:
roles[msg.sender][9] = true;
It's not clear what the number 9 means.
In RoleAware.sol there is a constant with the value 9:
uint256 constant TOKEN_ACTIVATOR = 9;
Impact
The code is more difficult to read without an explanation for the number 9.
In case the code would be refactored in the future and the constants in RoleAware.sol are renumbered, the value in Roles.sol would no longer correspond to the right value.
Recommended mitigation steps
Move the constants from Roles.sol to RoleAware.sol and replace 9 with the appropriate constant.
No function for TOKEN_ADMIN in RoleAware.sol
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
This is a minor suggestion.
RoleAware.sol contains functions for most of the constants. However the one exception is TOKEN_ADMIN.
Impact
The code is slightly longer than necessary.
Recommended mitigation steps
Remove the constant TOKEN_ADMIN, or provide a comment why it isn't used.
Price feed can be manipulated
Vulnerability details
Anyone can trigger an update to the price feed by calling PriceAware.getCurrentPriceInPeg(token, inAmount, forceCurBlock=true)
.
If the update window has passed, the price will be computed by simulating a Uniswap-like trade with the amounts.
This simulation uses the reserves of the Uniswap pairs which can be changed drastically using flash loans to yield almost arbitrary output amounts, and thus prices.
Impact
Wrong prices break the core functionality of the contracts such as borrowing on margin, liquidations, etc.
Recommended mitigation steps
Do not use the Uniswap spot price as the real price.
Uniswaps itself warns against this and instead recommends implementing a TWAP price oracle using the price*CumulativeLast
variables.
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Natspec comments not used in a consistent way
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
This is a minor suggestion.
The comments do not comply perfectly to the natspec specification.
Too many or too few /'s
Proof of concept
Here are a few examples:
MarginRouter.sol: // @dev internal helper swapping ...
MarginRouter.sol: //// @dev external function ...
MarginRouter.sol: /// about a trade
There should be exactly three /'s before an @.. keyword
If there is no @.. keyword then there should be two /'s
Tools used
grep " // @" *
grep "//// @" *
grep "/// [^@]" *
Recommended mitigation steps
Check and update the comments to comply with the natspec comment
Note in the latest solidity version you can also use
@Custom:...
everywhere within the source
Impact
The documentation generated using the natspec lines might not be accurate.
[INFO] All caps indicates that the value should be constant
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
All caps indicates that the value should be constant:
uint256 public MAINTAINER_CUT_PERCENT = 5;
However, it can be changed with function setMaintainerCutPercent. Then, this comment may become innacurate: // 5% of value borrowed Same with UPDATE_RATE_PERMIL, UPDATE_MAX_PEG_AMOUNT, UPDATE_MIN_PEG_AMOUNT.
Impact
Recommended mitigation steps
Missing `fromToken != toToken` check in `MarginRouter.crossSwapExactTokensForTokens`/`MarginRouter.crossSwapTokensForExactTokens`
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
Attacker calls MarginRouter.crossSwapExactTokensForTokens
with a fake pair and the same token[0] == tokne[1].
crossSwapExactTokensForTokens(1000 WETH, 0, [ATTACKER_CONTRACT], [WETH, WETH])
. When the amounts are computed by the amounts = UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens);
call, the attacker contract returns fake reserves that yield 0 output
When _swapExactT4T
is called, the funds are sent to the fake contract and doing nothing passes all checks in _swap
call that follows because the startingBalance
is stored after the initial Fund withdraw to the pair.
function _swapExactT4T() {
// withdraw happens here
Fund(fund()).withdraw(tokens[0], pairs[0], amounts[0]);
_swap(amounts, pairs, tokens, fund());
}
function _swap() {
uint256 startingBalance = IERC20(outToken).balanceOf(_to);
uint256 endingBalance = IERC20(outToken).balanceOf(_to);
// passes as startingBalance == endingBalance + 0
require(
endingBalance >= startingBalance + amounts[amounts.length - 1],
"Defective AMM route; balances don't match"
);
}
Impact
The full impact is not yet known as registerTrade
could still fail when subtracting the inAmount
and adding 0 outAmount
.
At least, this attack is similar to a withdrawal which is supposed to only occur after a certain coolingOffPeriod
has passed, but this time-lock is circumvented with this attack.
Recommended mitigation steps
Move the fund withdrawal to the first pair after the startingBalance
assignment.
Check fromToken != toToken
as cyclical trades (arbitrages) are likely not what margin traders are after.
Consider if the same check is required for registerTradeAndBorrow
/ adjustAmounts
functions.
maintainer can be pushed out
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
The function liquidate (in both CrossMarginLiquidation.sol and IsolatedMarginLiquidation.sol) can be called by everyone.
If an attacker calls this repeatedly then the maintainer will be punished and eventually be reported as maintainerIsFailing
And then the attacker can take the payouts
Proof of concept
When a non authorized address repeatedly calls liquidate then the following happens:
isAuthorized = false
which means maintenanceFailures[currentMaintainer] increases
after sufficient calls it will be higher than the threshold and then
maintainerIsFailing() will be true
This results in canTakeNow being true
which finally means the following will be executed:
Fund(fund()).withdraw(PriceAware.peg, msg.sender, maintainerCut);
Impact
An attacker can push out a maintainer and take over the liquidation revenues
Tools used
remix
Recommended mitigation steps
put authorization on who can call the liquidate function
review the maintainer punishment scheme
Re-entrancy bug in `MarginRouter.crossSwapExactTokensForTokens` allows inflating balance
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
One can call the MarginRouter.crossSwapExactTokensForTokens
function first with a fake contract disguised as a token pair:
crossSwapExactTokensForTokens(0.0001 WETH, 0, [ATTACKER_CONTRACT], [WETH, WBTC])
. When the amounts are computed by the amounts = UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens);
call, the attacker contract returns fake reserves that yield 1 WBTC for the tiny input.
The resulting amount is credited through registerTrade
.
Afterwards, _swapExactT4T([0.0001 WETH, 1 WBTC], 0, [ATTACKER_CONTRACT], [WETH, WBTC])
is called with the fake pair and token amounts.
At some point _swap
is called, the starting balance is stored in startingBalance
, and the attacker contract call allows a re-entrancy:
pair.swap(0.0001 WETH, 1 WBTC, FUND, new bytes(0)); // can re-enter here
From the ATTACKER_CONTRACT we re-enter the MarginRouter.crossSwapExactTokensForTokens(30 WETH, 0, WETH_WBTC_PAIR, [WETH, WBTC])
function with the actual WETH <> WBTC pair contract.
All checks pass, the FUND receives the actual amount, the outer _swap
continues execution after the re-entrancy and the endingBalance >= startingBalance + amounts[amounts.length - 1]
check passes as well because the inner swap successfully deposited these funds.
We end up doing 1 real trade but being credited twice the output amount.
Impact
This allows someone to be credited multiples of the actual swap result. This can be repeated many times and finally, all tokens can be stolen.
[INFO] Optimize the inheritance tree
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
Optimize the inheritance tree. For example:
contract Lending is
BaseLending,
HourlyBondSubscriptionLending,
BondLending,
...
abstract contract BondLending is BaseLending
abstract contract HourlyBondSubscriptionLending is BaseLending
so Lending already inherits BaseLending from BondLending and HourlyBondSubscriptionLending.
Isolated margin contracts declare but do not set the value of liquidationThresholdPercent
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
CrossMarginTrading sets value of liquidationThresholdPercent in the constructor:
liquidationThresholdPercent = 110;
Isolated margin contracts declare but do not set the value of liquidationThresholdPercent.
Recommended mitigation steps
Set the initial value for the liquidationThresholdPercent in Isolated margin contracts.
Impact
This makes function belowMaintenanceThreshold to always return true unless a value is set via function setLiquidationThresholdPercent. Comments indicate that the value should also be set to 110:
// The following should hold:
// holdings / loan >= 1.1
// => holdings >= loan * 1.1
This is an example finding
Email address
Handle
adamavenir
Eth address
123123123
Vulnerability details
Some details:
details(schemtails)
Impact
Brace for it!
Proof of concept
- proof
- of
- concept
Tools used
I used no tools except this form and my BARE HANDS!
Recommended mitigation steps
I would recommend not doing this bug.
The lambda function in the form should now be secure...
Email address
Handle
adamavenir
Eth address
123124124
Vulnerability details
1
Impact
2
Proof of concept
3
Tools used
4
Recommended mitigation steps
5
`account.holdsToken` is never set
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The addHolding
function does not update the account.holdsToken
map.
function addHolding(
CrossMarginAccount storage account,
address token,
uint256 depositAmount
) internal {
if (!hasHoldingToken(account, token)) {
// SHOULD SET account.holdsToken here
account.holdingTokens.push(token);
}
account.holdings[token] += depositAmount;
}
This leads to a critical vulnerability where deposits of the same token keep being pushed to the account.holdingTokens
array but the sum is correctly updated in account.holdings[token]
.
However, because of the duplicate token in the holdingTokens
array the same token is counted several times in the getHoldingAmounts
function:
function getHoldingAmounts(address trader)
external
view
override
returns (
address[] memory holdingTokens,
uint256[] memory holdingAmounts
)
{
CrossMarginAccount storage account = marginAccounts[trader];
holdingTokens = account.holdingTokens;
holdingAmounts = new uint256[](account.holdingTokens.length);
for (uint256 idx = 0; holdingTokens.length > idx; idx++) {
address tokenAddress = holdingTokens[idx];
// RETURNS SUM OF THE BALANCE FOR EACH TOKEN ENTRY
holdingAmounts[idx] = account.holdings[tokenAddress];
}
}
The MarginRouter.crossCloseAccount
function uses these wrong amounts to withdraw all tokens:
function crossCloseAccount() external {
(address[] memory holdingTokens, uint256[] memory holdingAmounts) =
IMarginTrading(marginTrading()).getHoldingAmounts(msg.sender);
// requires all debts paid off
IMarginTrading(marginTrading()).registerLiquidation(msg.sender);
for (uint256 i; holdingTokens.length > i; i++) {
Fund(fund()).withdraw(
holdingTokens[i],
msg.sender,
holdingAmounts[i]
);
}
}
Impact
An attacker can just deposit the same token X times which increases their balance by X times the actual value.
This inflated balance can then be withdrawn to steal all tokens.
Recommended mitigation steps
Correctly set the account.holdsToken
map in addHolding
.
No default `liquidationThresholdPercent`
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The IsolatedMarginTrading
contract does not define a default liquidationThresholdPercent
which means it is set to 0.
The belowMaintenanceThreshold
function uses this value and anyone could be liquidated due to 100 * holdings >= liquidationThresholdPercent * loan = 0
being always true.
Impact
Anyone can be liquidated immediately.
If the faulty belowMaintenanceThreshold
function is fixed (see other issue), then nobody could be liquidated which is bad as well.
Recommended mitigation steps
Set a default liquidation threshold like in CrossMarginTrading
contracts.
Users are credited more tokens when paying back debt with `registerTradeAndBorrow`
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
The registerTradeAndBorrow
is called with the results of a trade (inAmount
, outAmount
). It first tries to pay back any debt with the outAmount
.
However, the full outAmount
is credited to the user again as a deposit in the adjustAmounts(account, tokenFrom, tokenTo, sellAmount, outAmount);
call.
Impact
As the user pays back their debt and is credited the same amount again, they are essentially credited twice the outAmount
, making a profit of one outAmount
.
This can be withdrawn and the process can be repeated until the funds are empty.
Recommended mitigation steps
In the adjustAmounts
call, it should only credit outAmount - extinguishableDebt
as a deposit like in registerDeposit
.
The registerDeposit
function correctly handles this case.
Re-entrancy bug in `MarginRouter.crossSwapTokensForExactTokens` allows inflating balance
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
One can call the MarginRouter.crossSwapExactTokensForTokens
function first with a fake contract disguised as a token pair:
crossSwapExactTokensForTokens(0.0001 WETH, 0, [ATTACKER_CONTRACT], [WETH, WBTC])
. When the amounts are computed by the amounts = UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens);
call, the attacker contract returns fake reserves that yield 1 WBTC for the tiny input.
The resulting amount is credited through registerTrade
.
Afterwards, _swapExactT4T([0.0001 WETH, 1 WBTC], 0, [ATTACKER_CONTRACT], [WETH, WBTC])
is called with the fake pair and token amounts.
At some point _swap
is called, the starting balance is stored in startingBalance
, and the attacker contract call allows a re-entrancy:
pair.swap(0.0001 WETH, 1 WBTC, FUND, new bytes(0)); // can re-enter here
From the ATTACKER_CONTRACT we re-enter the MarginRouter.crossSwapExactTokensForTokens(30 WETH, 0, WETH_WBTC_PAIR, [WETH, WBTC])
function with the actual WETH <> WBTC pair contract.
All checks pass, the FUND receives the actual amount, the outer _swap
continues execution after the re-entrancy and the endingBalance >= startingBalance + amounts[amounts.length - 1]
check passes as well because the inner swap successfully deposited these funds.
We end up doing 1 real trade but being credited twice the output amount.
Impact
This allows someone to be credited multiples of the actual swap result. This can be repeated many times and finally, all tokens can be stolen.
Recommended mitigation steps
Add re-entrancy guards (from OpenZeppelin) to all external functions of MarginRouter
.
There might be several attack vectors of this function as the attacker controls many parameters.
The idea of first doing an estimation with UniswapStyleLib.getAmountsOut(amountIn - fees, pairs, tokens)
and updating the user with these estimated amounts, before doing the actual trade, feels quite vulnerable to me.
Consider removing the estimation and only doing the actual trade first, then calling registerTrade
with the actual trade amounts returned.
Unlocked Pragma
Email address
Handle
@cmichelio
Eth address
0x6823636c2462cfdcD8d33fE53fBCD0EdbE2752ad
Vulnerability details
Every Solidity file specifies in the header a version number of the format pragma solidity ^0.8.0
. The caret (^
) before the version number implies an unlocked pragma, meaning that the compiler will use the specified version or above.
Itβs usually a good idea to pin a specific version to know what compiler bug fixes and optimizations were enabled at the time of compiling the contract.
Impact
Recommended mitigation steps
Pin the compiler versions.
function crossWithdrawETH does not emit withdraw event
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
contract MarginRouter, function crossWithdrawETH does not emit withdraw event:
emit CrossWithdraw(msg.sender, WETH, withdrawAmount);
Impact
no impact on security.
Recommended mitigation steps
emit the expected event
Todo's left in code
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
This is a minor suggestion.
Several TODO's are left in the code:
IsolatedMarginAccounts.sol: // TODO check if underflow?
IsolatedMarginAccounts.sol: // TODO TELL LENDING
IsolatedMarginLiquidation.sol: // TODO pay off / extinguish that loan
Lending.sol:// TODO activate bonds for lending
Lending.sol:// TODO disburse token if isolated bond issuer
MarginRouter.sol: // TODO minimum trade?
Impact
TODO usually mean something still have to be checked of done. This could lead to vulnerabilities if not verified.
Tools used
grep
Recommended mitigation steps
Check the TODO's and fix if necessary. Remove them afterwards
[INFO] Useless overflow comments
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
I think this comment is useless when Solidity 0.8 is used (protection from overflow by default):
// no overflow because depositAmount >= extinguishableDebt
uint256 addedHolding = depositAmount - extinguishableDebt;
there are more such comments, for example:
/// won't overflow
borrowAmount = inAmount - sellAmount;
Multisig wallets can't be used for liquidate
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
The function liquidate, which is defined in both
CrossMarginLiquidation.sol and IsolatedMarginLiquidation.sol, includes the modifier noIntermediary.
This modifier prevents the use of Multisig wallets.
Impact
If the maintainer happens to use a multisig wallet he might not experience any issues until he tries to call the function liquidate. At that moment he can't successfully call the function.
Recommended mitigation steps
Verify if the prevention to use multisig wallets is intentional. In that case add a comment to the liquidate functions.
If it is not intentional update the code so multisigs wallets can be supported.
Owner can initialize an already initialized tranche
Email address
Handle
s1m0
Eth address
0x9b3E9e3E4a174d59279FC7cd268e035992412384
Vulnerability details
The owner can initialize an already initialized tranche by calling setTranche https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L78 with 0 as share argument and then calling initTranche https://github.com/code-423n4/marginswap/blob/main/contracts/IncentiveDistribution.sol#L101 bypassing the check require(tm.rewardShare == 0, "Tranche already initialized");
Recommended mitigation steps
Check share != 0 for setTrancheShare and initTranche
Impact
The state of the system would become not correct by inflating the allTranches variable and it would raise the gas cost for calling withdrawReward
Tools used
Manual analysis
Proof of concept
Assuming the 1 tranche is initialized.
- call setTrancheShare(1, 0)
- call initTranche(1, n)
function initTranche should check that the share parameter is > 0
Impact
only admin can call this so highly unlikely to happen yet it would be better if code prevents that.
Recommended mitigation steps
require share to be greater than 0.
Tools used
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
function initTranche should check that the "share" parameter is > 0, otherwise, it may be possible to initialize the same tranche again.
isStakePenalizer differtent than other functions in RoleAware.sol
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
This is a minor suggestion.
The function isStakePenalizer in RoleAware.sol uses roles.getRole...
However all other function use roleCache...
It's not clear why this difference exists.
Impact
If roleCache could also be used a tiny amount of gas could be safed.
Proof of concept
Recommended mitigation steps
Check if isStakePenalizer can use roleCache, in that case update the source.
Otherwise provide a comment why roles.getRole is neccesary
Different solidity version in UniswapStyleLib.sol
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
The solidity version in UniswapStyleLib.sol (>=0.5.0) is different than the solidity version in the other contracts (e.g. ^0.8.0)
Also math actions are present in the functions getAmountOut and getAmountIn that could easily lead to an underflow or division by 0; (note safemath is not used).
Note: In solidity 0.8.0 safemath like protections are default.
Impact
The impact is low because UniswapStyleLib is a library and the solidity version of the contract that uses the library is used (e.g. ^0.8.0), which has safemath like protections.
It is cleaner to have the same solidity version everywhere.
Proof of concept
getAmountIn(3,1,1000) would give division by 0
getAmountIn(1,1,1) will underflow denominator
Recommended mitigation steps
Use the same solidity version everywhere
[INFO] Code duplication in viewCurrentMaintenanceStaker
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
This code has too much duplication:
if (maintenanceStakePerBlock > currentStake) {
// skip
staker = nextMaintenanceStaker[staker];
currentStake = getMaintenanceStakerStake(staker);
} else {
startBlock += currentStake / maintenanceStakePerBlock;
staker = nextMaintenanceStaker[staker];
currentStake = getMaintenanceStakerStake(staker);
}
and can be refactored to:
if (maintenanceStakePerBlock <= currentStake) {
startBlock += currentStake / maintenanceStakePerBlock;
}
staker = nextMaintenanceStaker[staker];
currentStake = getMaintenanceStakerStake(staker);
[INFO] TODOs
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
There are 6 TODOs left. It makes it confusing to audit such code.
function buyBond charges msg.sender twice
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
function buyBond transfers amount from msg.sender twice:
Fund(fund()).depositFor(msg.sender, issuer, amount);
...
collectToken(issuer, msg.sender, amount);
Impact
This makes the msg.sender pay twice for the same bond.
Recommended mitigation steps
Charge poor man only once.
[INFO] Misleading revert messages
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
There are misleading revert messages, for example: "Calling contract not authorized to deposit" on functions like registerUnwind or registerCloseAccount.
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Handle
paulius.eth
Email address
Several function have no entry check
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
The following functions have no entry check or a trivial entry check:
withdrawHourlyBond Lending.sol
closeHourlyBondAccount Lending.sol
haircut Lending.sol
addDelegate(own adress...) Admin.sol
removeDelegate(own adress...) Admin.sol
depositStake Admin.sol
disburseLiqStakeAttacks CrossMarginLiquidation.sol
disburseLiqStakeAttacks IsolatedMarginLiquidation.sol
getCurrentPriceInPeg PriceAware.sol
Impact
By manipulating the input values (for example extremely large values)
you might be able to disturb the internal administration of the contract, thus perhaps locking function or giving wrong rates.
note: function haircut is trivial so hardly any risk
Recommended mitigation steps
Check the functions to see if they are completely risk free and add entry checks if they are not.
Add a comment to notify the function is meant to be called by everyone.
Proof of concept
Based on source code review.
A real attack requires the deployed code to be able to construct the right values.
lastUpdatedDay not initialized
Email address
Handle
gpersoon
Eth address
gpersoon.eth
Vulnerability details
The variable lastUpdatedDay in IncentiveDistribution.sol is not (properly) initialized.
This means the function updateDayTotals will end up in a very large loop which will lead to an out of gas error.
Even if the loop would end, the variable currentDailyDistribution would be updated very often.
Thus updateDayTotals cannot be performed
Impact
The entire IncentiveDistribution does not work.
If the loop would stop, the variable currentDailyDistribution is not accurate, resulting in a far lower incentive distribution than expected.
Recommended mitigation steps
Initialize lastUpdatedDay with something like block.timestamp / (1 days)
Proof of concept
uint256 lastUpdatedDay; # ==> lastUpdatedDay = 0
#When the function updateDayTotals is called:
uint256 public nowDay = block.timestamp / (1 days); #==> ~ 18721
uint256 dayDiff = nowDay - lastUpdatedDay; #==> 18721-0 = 18721
for (uint256 i = 0; i < dayDiff; i++) { # very long loop (18721)
currentDailyDistribution = ....
}
#will result in an out of gas error
[INFO] setUpdateMaxPegAmount and setUpdateMinPegAmount do not check boundaries
Email address
Handle
paulius.eth
Eth address
0x523B5b2Cc58A818667C22c862930B141f85d49DD
Vulnerability details
This is FYI, not a real issue as you have expressed your interest in minor improvement suggestions (not security or gas related):
setUpdateMaxPegAmount should check that amount >= UPDATE_MIN_PEG_AMOUNT and setUpdateMinPegAmount should check that amount <= UPDATE_MAX_PEG_AMOUNT. But only owner can change these values so no real issue.
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. πππ
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google β€οΈ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.