GithubHelp home page GithubHelp logo

2021-05-yield-findings's People

Contributors

c4-staff avatar code423n4 avatar joshuashort avatar ninek9 avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

2021-05-yield-findings's Issues

Undercollateralized vaults' owner can be overwritten

Handle

cmichel

Vulnerability details

The witch can Witch.grab vaults and the vaultOwners[vaultId] field is set to the original owner.
However, when the auction time is over and the debt has not been fully paid back, the original owner is not restored, and the witch can grab the same vault again, overwriting the original owner vaultOwners[vaultId] field permanently with the witch.

function grab(bytes12 vaultId) public {
    DataTypes.Vault memory vault = cauldron.vaults(vaultId);
    vaultOwners[vaultId] = vault.owner;
    cauldron.grab(vaultId, address(this));
}

Even a full repayment will not restore the original vault owner anymore.

Impact

No funds will be stuck as the vault can still be correctly liquidated (calling settle).
However, the vault owner will not be restored which is bad if it is a valuable vaultId (low number) that has a special meaning or would be used as an NFT/for retroactive airdrops for initial liquidity providers down the road.

Recommended Mitigation Steps

When grabbing check if vaultOwners[vaultId] is already the witch and in that case just do an early return of the function - not overwriting the vaultOwners[vaultId] field.

Use ".selector" instead of hex number

Handle

gpersoon

Vulnerability details

Impact

In the contract SafeERC20Namer.sol a few function selects are encoded as hexadecimal numbers.
Solidity also has the keyword ".selector" which makes the code easier to read and less error prone.

Note: TransferHelper.sol already uses this construct:
// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/token/TransferHelper.sol#L22

Proof of Concept

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/token/SafeERC20Namer.sol#L74

    // 0x95d89b41 = bytes4(keccak256("symbol()"))
    string memory symbol = callAndParseStringReturn(token, 0x95d89b41);

    // 0x06fdde03 = bytes4(keccak256("name()"))
    string memory name = callAndParseStringReturn(token, 0x06fdde03);

    // 0x313ce567 = bytes4(keccak256("decimals()"))
    (bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(0x313ce567));

Tools Used

Editor

Recommended Mitigation Steps

Alternative implementations:
IERC20Metadata.symbol.selector // 0x95d89b41 = bytes4(keccak256("symbol()"))
IERC20Metadata.name.selector // 0x06fdde03 = bytes4(keccak256("name()"))
IERC20Metadata.decimals.selector // 0x313ce567 = bytes4(keccak256("decimals()"))

Missing sender address check in receive() may lead to locked Ether

Handle

0xRajeev

Vulnerability details

Impact

Add an address check in receive() of Ladle.sol to ensure the only address sending ETH being received in receive() is the Weth9 contract (similar to the check in PoolRouter.sol) for Ether withdrawal in _exitEther().

This will prevent stray Ether from being sent accidentally to this contract and getting locked.

Proof of Concept

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L521-L522

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/yieldspace/PoolRouter.sol#L145-L148

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add an address check in receive() of Ladle.sol to ensure only Weth9 contract can send Ether to this contract.

enum TokenType is never used

Handle

pauliax

Vulnerability details

Impact

enum TokenType in library PoolDataTypes is not used anywhere.

Recommended Mitigation Steps

Either remove it or use it where intended.

Constants "chi" and "rate"

Handle

gpersoon

Vulnerability details

Impact

Several implementations of the value of "chi" and "rate" are used, sometimes as constant and sometimes the direct value is used, see proof of concept below.
The risk is that if it is changed in one place if might not be changed in another place, leading to bugs.

Proof of Concept

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Wand.sol#L26
bytes6 public constant CHI = "chi";
bytes6 public constant RATE = "rate";

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/FYToken.sol#L27
bytes32 constant CHI = "chi";

//https://github.com/code-423n4/2021-05-yield/blob/main/contracts/oracles/compound/CompoundMultiOracle.sol#L40
function _peek(bytes6 base, bytes6 kind) private view returns (uint price, uint updateTime) {
...
if (kind == "rate") rawPrice = CTokenInterface(source).borrowIndex();
else if (kind == "chi") rawPrice = CTokenInterface(source).exchangeRateStored();

Tools Used

grep

Recommended Mitigation Steps

Define the constants for "chi" and "rate" on one location and include this where required.

Inefficient Witch buy

Handle

cmichel

Vulnerability details

In Witch.buy there's the possibility to do one multiplication instead of two divisions: Instead of computing the ink price as 1 / artPrice and then dividing by it to get the ink amount as ink = art / price, just keep the art price and multiply it by the art amount.
These lines need to be changed:

price = term1.wmul(term2); // this is the art price in terms of ink now, instead of ink price
ink = uint256(art).wmulup(price); // can just multiply by art price

Impact

One saves gas by doing one multiplication instead of two divisions which seems to be an important goal of Yield v2.

Flashloan griefing attack

Handle

cmichel

Vulnerability details

Funds from contracts that approved a join and implement the flashloan interface can be stolen.
One can call Join.flashLoan(vulnerable_contract, token, amount) and the contract's balance will be decreased by the fees they have to pay for the flashloan. One can repeat this until the contract's balance is emptied.

Impact

Funds from contracts that approved a join and implement the flashloan interface can be "burned".

Recommended Mitigation Steps

Don't allow taking flashloans on behalf of another account, or don't allow join to transferFrom, i.e., let the receiver explicitly push the funds.

`Cauldron.addSeries` does not check if fyToken is already in use

Handle

cmichel

Vulnerability details

The Cauldron.addSeries function allows using the same fyToken for multiple series. It also does not check the series/maturity even though the error message states it require (fyToken.underlying() == base, "Mismatched series and base");

Impact

Using the same fyToken on several series could scatter liquidity and lead to other issues depending on how the Joins are set up.

Recommended Mitigation Steps

Make sure to only create new series using Wand.addSeries that creates a new fyToken for every series.

Implicit unsafe math

Handle

cmichel

Vulnerability details

Ladle._close (and many other occurrences) reverts the transaction on certain signed inputs that are negated and cast to unsigned integers.

// Ladle._close calling it with art or ink as type(int128).min will crash
uint128 amt = _debtInBase(vault.seriesId, series, uint128(-art));
ilkJoin.exit(to, uint128(-ink))

// explanation
int128 art = type(int128).min; // -2^127
uint128 amt = uint128(-art); // this fails as -art=--2^127=2^127 cannot be represented in int128

Other places:

  • CauldronMath.add
  • Ladle._pour
  • everywhere where -int* is used

Impact

One cannot use the actual type(int128).min value for function parameters.

Recommended Mitigation Steps

Revert with a meaningful error message as is done in the /math/Cast* functions.

Return values of batch operations are ignored

Handle

0xRajeev

Vulnerability details

Impact

Many batched operation functions return values but these are ignored by the caller batch(). While this may be acceptable for the front-end which picks up any state changes from such functions via emitted events, integrating protocols that make a call to batch() may require it to package and send back return values of all operations from the batch to react on-chain to the success/failure or other return values from such calls. Otherwise, they will be in the dark on the success/impact of batched operations they’ve triggered.

Proof of Concept

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L120-L245

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L250

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L258

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L284-L286

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L296-L298

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L326-L328

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L342-L344

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L382-L384

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L396-L398

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L410-L412

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L446-L448

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L462-L464

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L527-L529

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L539-L541

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L559-L561

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L588-L590

Tools Used

Manual Analysis

Recommended Mitigation Steps

Package and send back return values of all batched operations’ functions to the caller of batch().

Avoid assembly in getRevertMsg

Handle

gpersoon

Vulnerability details

Impact

The function getRevertMsg of RevertMsgExtractor.sol uses assembly to retrieve revert information.
The latest solidity version have new functions that allows you to retrieve information without assembly.

Proof of Concept

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/RevertMsgExtractor.sol
function getRevertMsg(bytes memory returnData) internal pure returns (string memory) {
.. assembly {
// Slice the sighash.
returnData := add(returnData, 0x04)
}

Tools Used

Recommended Mitigation Steps

Below is a piece of code showing the new functionality:

pragma solidity ^0.8.1;

contract ContractError {
function Underflow() public pure returns (uint) {
uint x = 0;
x--; // this will generate an underflow
return x;
}
function UncheckedUnderflow() public pure returns (uint) {
uint x = 0;
unchecked { x--; } // this will generate an underflow
return x;
}
}

contract C {
ContractError e = new ContractError();

function TestUnderflow() public view returns (string memory) {
     try e.Underflow() returns (uint) {
        return "Ok";
    } catch Error(string memory reason) {
        return reason;
    } catch Panic(uint _code) {
        if (_code == 0x01) { return "Assertion failed"; }
        else if (_code == 0x11) { return "Underflow/overflow"; }
        // We ignore the other errors.
        return "Other Panic";
    } catch (bytes memory reason) { 
        uint x=0;
        for (uint i=0;i<4;i++) //get first 4 bytes
            x = (x<<8) + uint(uint8(reason[i]));
    
        if (x == 0x08c379a0) // abi.encodeWithSignature("Error(string)")
            return "Error";
        return "Unknown";
    }
}

}

PoolFactory and JoinFactory very similar

Handle

gpersoon

Vulnerability details

Impact

PoolFactory and JoinFactory contain very similar but also relatively complicated code.
// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/yieldspace/PoolFactory.sol
// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/JoinFactory.sol

The risk is that future changes/improvements in one contract might not be updated in the other.

Proof of Concept

Tools Used

Editor

Recommended Mitigation Steps

Consider refactoring the code where the core code is put in a library and reused from both of the contracts.

Use constants for numbers

Handle

gpersoon

Vulnerability details

Impact

In several locations in the code numbers like 1e12, 1e18, 1e27 are used. The same goes for values like: type(uint256).max
It quite easy to make a mistake somewhere, also when comparing values.

Proof of Concept

.\Cauldron.sol: (uint256 rateAtMaturity,) = rateOracle.get(series_.baseId, bytes32("rate"), 1e18);
.\Cauldron.sol: (uint256 rate,) = rateOracle.get(series_.baseId, bytes32("rate"), 1e18);
.\Cauldron.sol: accrual_ = accrual_ >= 1e18 ? accrual_ : 1e18; // The accrual can't be below 1 (with 18 decimals)
.\Cauldron.sol: uint256 ratio = uint256(spotOracle_.ratio) * 1e12; // Normalized to 18 decimals
.\FYToken.sol: (chiAtMaturity,) = oracle.get(underlyingId, CHI, 1e18);
.\FYToken.sol: (uint256 chi,) = oracle.get(underlyingId, CHI, 1e18);
.\FYToken.sol: accrual
= accrual_ >= 1e18 ? accrual_ : 1e18; // The accrual can't be below 1 (with 18 decimals)
.\Witch.sol: require (initialProportion_ <= 1e18, "Only at or under 100%");
.\Witch.sol: uint256 term2 = initialProportion_ + (1e18 - initialProportion_).wmul(dividend2.wdiv(divisor2));
.\Witch.sol: price = uint256(1e18).wdiv(term1.wmul(term2));
.\oracles\chainlink\ChainlinkMultiOracle.sol: value = price * amount / 1e18;
.\oracles\chainlink\ChainlinkMultiOracle.sol: value = price * amount / 1e18;
.\oracles\compound\CompoundMultiOracle.sol: value = price * amount / 1e18;
.\oracles\compound\CompoundMultiOracle.sol: value = price * amount / 1e18;
.\yieldspace\Pool.sol: uint256 scaledFYTokenCached = uint256(_fyTokenCached) * 1e27;
.\yieldspace\YieldMath.sol: result = result > 1e12 ? result - 1e12 : 0; // Subtract error guard, flooring the result at zero
.\yieldspace\YieldMath.sol: result = result > 1e12 ? result - 1e12 : 0; // Subtract error guard, flooring the result at zero
.\yieldspace\YieldMath.sol: result = result < MAX - 1e12 ? result + 1e12 : MAX; // Add error guard, ceiling the result at max
.\yieldspace\YieldMath.sol: result = result < MAX - 1e12 ? result + 1e12 : MAX; // Add error guard, ceiling the result at max

.\FYToken.sol: uint256 public chiAtMaturity = type(uint256).max; // Spot price (exchange rate) between the base and an interest accruing token at maturity
.\FYToken.sol: require (chiAtMaturity == type(uint256).max, "Already matured");
.\FYToken.sol: if (chiAtMaturity == type(uint256).max) { // After maturity, but chi not yet recorded. Let's record it, and accrual is then 1.

Tools Used

grep

Recommended Mitigation Steps

Define constants for the numbers used throughout the code.

ERC20 approve is vulnerable to the front-running

Handle

pauliax

Vulnerability details

Impact

function approve is vulnerable to the front-running. This issue is described here: https://blog.smartdec.net/erc20-approve-issue-in-simple-words-a41aaf47bca6 A malicious delegate can scout for a change in approval and front-run that. It is more of a theoretical issue but still I want you to be aware of this and that.

Recommended Mitigation Steps

It is recommended introducing increaseAllowance / decreaseAllowance functions.

Contract Factory Replace

Handle

0xsomeone

Vulnerability details

Impact

The PoolFactory contract is utilizing the create2 OPCODE (via syntactic sugar) to deploy a new Pool instance, however, no sanitization occurs on the inputs allowing contracts and thereby ownerships to be replaced at will.

Proof of Concept

If the createPool function is invoked with the same base and fyToken, it will replace any existing Pool in the specified address with a new instance whose ownership will be transferred to the caller. This breaks the logical assumption that ownership of a pool should be retained and dictated by the currently-active owner of the contract.

Referenced Code: https://github.com/code-423n4/2021-05-yield/blob/main/contracts/yieldspace/PoolFactory.sol#L68-L80

Tools Used

Manual Review.

Recommended Mitigation Steps

A require check should be imposed prohibiting deployments of already-created Pools by either utilizing a mapping for the hashes that is set to true or by dynamically evaluating whether a contract already exists at the specified address via an isContract invocation.

borrowingFee is not initialized

Handle

0xRajeev

Vulnerability details

Impact

borrowingFee not initialized (defaults to 0) at declaration and depends on setFee() for a non-zero acceptable value.

It is safer to initialize at declaration to a non-zero default otherwise borrowers can borrow for zero fees.

Proof of Concept

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/LadleStorage.sol#L37

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L304

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L438

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L105-L112

Tools Used

Manual Analysis

Recommended Mitigation Steps

  1. Initialize borrowingFee at declaration to a non-zero default
  2. Add a threshold check for the same in setFee()

Missing reentrancy guard and contract existence check for modules


Handle

0xRajeev

Vulnerability details

Impact

Protocol allows users to call registered modules via delegateCall in Module operation. It is not clear how these modules are validated before registration. If they are malicious they could cause reentrancy in the batch() call because there is no reentrancy guard protection. If they are destructed, delegateCall will still return success because low-level calls do not check for contract existence. Both will cause an undetermined level of impact to the protocol but the likelihood is low given the registration process and assumed validation there.

Proof of Concept

Eve manages to get her malicious module registered which causes reentrancy or maliciously affects protocol accounts/operations due to the delegateCall.

Alternatively, Alice registers a benign module but then accidentally calls selfDestruct on it. The module delegation is successful but without any side-effect because it doesn’t exist anymore.

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L239-L241

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L587-L595

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L96-L103

Tools Used

Manual Analysis

Recommended Mitigation Steps

  1. Ensure during module registration that modules are trustworthy without any possibility to cause reentrancies or self-destruct.
  2. Add reentrancy guard on batch() and contract existence check on module before delegateCall

buyFYToken and buyBase do not reimburse leftovers

Handle

pauliax

Vulnerability details

Impact

functions buyFYToken and buyBase do not reimburse leftovers. It checks that the transferred amount is between min and max boundaries, however, it does not send back any excess amount back to the sender nor it accounts it in the _update function (e.g. it uses _baseCached + baseIn, not baseBalance) so basically these tokens will be left for bots to feed their hunger.

Recommended Mitigation Steps

Either reimburse the sender, e.g. send back baseBalance - _baseCached - baseIn, or account that in the _update function.

maxFlashLoan has no effect on flashLoan

Handle

pauliax

Vulnerability details

Impact

contract Join declares a function maxFlashLoan which should indicate a maximum amount of tokens that can be lended:
function maxFlashLoan(address token) public view override returns (uint256) {
return token == asset ? storedBalance : 0;
}
Depending on the token a maximum amount is either a storedBalance or 0. However, this limit is never enforced. function flashLoan does not check that amount is within the limit of maxFlashLoan, thus making this function useless or even misleading.

Recommended Mitigation Steps

In function flashLoan add a check:
require(amount <= maxFlashLoan(token), "...");

gas improvements toAsciiString

Handle

gpersoon

Vulnerability details

Impact

The function toAsciiString can be improved the be easier to read and use less gas.
// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/AddressStringUtil.sol

Proof of Concept

pragma solidity >=0.5.0;

contract test {
function toAsciiString(address addr, uint256 len) public pure returns (string memory) {
require(len % 2 == 0 && len > 0 && len <= 40, 'AddressStringUtil: INVALID_LEN');
bytes memory s = new bytes(len);
uint256 addrNum = uint256(uint160(addr));
for (uint256 ii = 0; ii < len ; ii +=2) {
uint8 b = uint8(addrNum >> (4 * (38 - ii)));
s[ii] = char(b >> 4);
s[ii + 1] = char(b & 0x0f);
}
return string(s);
}

function char(uint8 b) private pure returns (bytes1 c) {
    if (b < 10) {
        return bytes1(b + 0x30);
    } else {
        return bytes1(b + 0x37);
    }
}

}

Tools Used

Recommended Mitigation Steps

See proof of concept above for improved version

Missing zero-address validations


Handle

0xRajeev

Vulnerability details

Impact

While the codebase does a great job of input validation for parameters of all kinds and especially addresses, there are a few places where zero-address validations are missing. None of them are catastrophic, will result in obvious reverts and can be reset given the permissioned/controlled interactions with the contracts.

Nevertheless, it is helpful to add zero-address validations to be consistent and ensure high availability of the protocol with resistance to accidental misconfigurations.

Proof of Concept

  1. Spot oracle address:

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L124-L127

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Wand.sol#L78

  1. Grab receiver addresses for other liquidation engines besides the Witch (which is fine because it uses address(this) for receiver):

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L235-L240

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L349-L360

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Witch.sol#L53

  1. Spot oracle’s spotSource in makeIlk() [unlike the checks in makeBase()]:

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Wand.sol#L76-L77

  1. Oracle setSource() in Compound, Uniswap and Chainlink can zero-address validate the source, instead of at the callers in Wand:

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/oracles/compound/CompoundMultiOracle.sol#L22-L23

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/oracles/uniswap/UniswapV3Oracle.sol#L49-L60

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/oracles/chainlink/ChainlinkMultiOracle.sol#L29-L44

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add require() to zero-address validate the address parameters.

stir to self

Handle

gpersoon

Vulnerability details

Impact

The function stir of Cauldron.sol can be manipulated when from == to.
In that case the balance of "to" is increased while the balance of "from" isn't decreased.
This is due to the fact that a temporary variable is used and the balance of "to" overwrites the balance of "from".

Below is proof of concept with a simplified version of stir which shows the issue. The initial balance of 20 is increased to 25 without decreasing another balance.

Proof of Concept

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L268
contract test {

struct Balances {
    uint128 art;          // Debt amount
    uint128 ink;          // Collateral amount
}
mapping (uint => Balances) public balances; 
constructor() {
    balances[0]=Balances(20,20);
}
function stir(uint from, uint to, uint128 ink, uint128 art) public
{
    Balances memory balancesFrom = balances[from];
    Balances memory balancesTo = balances[to];
    if (ink > 0) {
        balancesFrom.ink -= ink;
        balancesTo.ink += ink;
    }
    if (art > 0) {
        balancesFrom.art -= art;
        balancesTo.art += art;
    }
    balances[from] = balancesFrom;
    balances[to] = balancesTo;

}
function TestStir() public returns(Balances memory) {
stir(0,0,5,5);
return balances[0]; // returns (25,25)
}
}

Tools Used

Editor

Recommended Mitigation Steps

Add a check to prevent that to and from are the same, for example
require (to != from, "To and From should be different");

'peek' and 'get' are identical (non-transactional)

Handle

pauliax

Vulnerability details

Impact

In the contract ChainlinkMultiOracle both functions 'peek' and 'get' are identical. They are declared as views while based on IOracle interface 'get' should be transactional.

FYTokens can be minted for free

Handle

cmichel

Vulnerability details

The core issue is that one can force the protocol to do an arbitrary trade in the pool using Ladle._roll. The function allows specifying a base amount and the protocol will mint as many fyTokens as needed for the trade, and trade them in the pool.

This can be used for a sandwich attack by forcing the protocol to mint fyTokens and trade them for underlying in an imbalanced pool with bad prices.

// Calculate debt in old fyToken terms
uint128 amt = _debtInBase(vault.seriesId, series, balances.art);

// Mint new fyToken to the pool, as a kind of flash loan
// @audit: loan can be set to 255
newFyToken.mint(address(pool), amt * loan);

// Buy the base required to pay off the debt in series 1, and find out the debt in series 2
// @audit: this buys the old debt (amt) at a bad price
newDebt = pool.buyBase(address(baseJoin), amt, max);
baseJoin.join(address(baseJoin), amt); // Repay the old series debt

pool.retrieveFYToken(address(newFyToken)); // Get the surplus fyToken
newFyToken.burn(address(newFyToken), (amt * loan) - newDebt); // Burn the surplus

The attack works like this:

  1. Create a vault for an oldSeries with some collateral (ink) and debt (art) (collateral can also be flashloaned)
  2. Flashloan lots of fyTokens of newSeries (for example totalSupply / 2)
  3. Dump them into the base <> fyToken pool (of newSeries) to receive base tokens. The pool is now imbalanced and has a large fyToken reserve and a low base reserve
  4. Call Ladle._roll(vaultId, vault, newSeriesId, loan=255, max=typeof(uint128).max) (using batch). This will calculate the amount of fyTokens needed to repay the vault's old debt (balances.art) which is a high value because of the unbalanced pool in 3). It then mints and swaps a large amount of fyTokens for the old debt amount. The pool's fyToken reserve has increased by a large amount again and the base tokens only by a (comparably) small amount.
  5. Perform the final sandwich attack trade by trading back the gained base amount from 3) in the pool. The trade will return a much larger fyToken amount than one had to pay in 3) due to the bad trade of the protocol at step 4). One makes a profit in fyTokens
  6. Repay the fyTokens flashloan. Use the profit to dump it further in the pool for more base tokens or redeem it later for base from the Join using fyToken.redeem.
    (7. Repay the vault setup flashloan)

This process can be repeated several times by rolling between two series.

As the pool behavior approximates Uniswap V2 with t -> maturity (see 6.3) this should work.

Impact

Note that there's currently no way to protect against this and the vulnerable trade can be triggered at will using _reroll. Therefore, one can use this to mint fyTokens for free which can then be used to dump them in the pool to steal the base reserve and redeem them in the Join at maturity to steal the funds in it.

Recommended Mitigation Steps

General sandwich attack advice (slippage): Consider checking the actual base/fyToken price from a TWAP oracle to compute the hypothetical base amount required for the trade. This amount can then be used as a slippage amount (instead of receiving max from the user). It could still be attackable even with a tight slippage amount as it can just be triggered repeatedly.

Specific advice: What about calculating oldDebtInbase the same way and then borrowing this amount in newFyTokens, minting and trading only this newFyTokens amount in the pool for base, and using the trade result base amount to repay (part of) the old debt. (Pseudocode: newDebt = _baseInDebt(newFyTokens, amt) and repaidDebt = _newFy2Base2OldFy(pool.sellFyToken(newDebt)) and the art diff for cauldron.roll would be newDebt - repaidDebt). This seems a lot safer than blindly minting fyTokens in the first place.

auth only works well with external functions

Handle

gpersoon

Vulnerability details

Impact

The auth modifier of AccessControl.sol doesn't work as you would expect.
It checks if you are authorized for "msg.sig", however msg.sig is the signature of the first function you have called, not of the current function.
So if you call function A, which calls function B, the "auth" modifier of function B checks if you are authorized for function A!

There is a difference between external an public functions. For external functions this works as expected because a fresh call (with a new msg.sig) is always made.
However with a public functions, which are called from within the same contract, this doesn't happen and the problem described above occurs.
See in the proof of concept for a piece of code which shows the problem.
In the code there are several functions which have public and auth combined, see also in the proof of concept .

In the current codebase I couldn't find a problem situation, however this could be accidentally introduced with future changes.
If could also be introduced via the _moduleCall of Ladle.sol, which allows functions to be defined which might call the public functions.

Proof of Concept

auth

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/access/AccessControl.sol#L90
modifier auth() {
require (_hasRole(msg.sig, msg.sender), "Access denied");
_;
}

example

pragma solidity ^0.8.0;
contract TestMsgSig {
event log(bytes4);

function setFeePublic(uint256) public  {
     emit log(this.setFeePublic.selector);
     emit log(msg.sig);
}
function setFeeExternal(uint256) external  {
     emit log(this.setFeeExternal.selector);
     emit log(msg.sig);
}

function TestPublic() public {
    setFeePublic(2);
}

function TestExternal() public {
   this.setFeeExternal(2);
}

}

occurrences of public auth

Wand.sol: function addAsset(bytes6 assetId,address asset) public auth {
Wand.sol: function makeBase(bytes6 assetId, IMultiOracleGov oracle, address rateSource, address chiSource) public auth {
Wand.sol: function makeIlk(bytes6 baseId, bytes6 ilkId, IMultiOracleGov oracle, address spotSource, uint32 ratio, uint96 max, uint24 min, uint8 dec) public auth {
Wand.sol: function addSeries(... ) public auth {
Witch.sol: function setAuctionTime(uint128 auctionTime_) public auth {
Witch.sol: function setInitialProportion(uint128 initialProportion_) public auth {
Ladle.sol: function setFee(uint256 fee) public auth
Join.sol: function setFlashFeeFactor(uint256 flashFeeFactor_) public auth {
oracles\chainlink\ChainlinkMultiOracle.sol: function setSource(bytes6 base, bytes6 quote, address source) public auth {
oracles\chainlink\ChainlinkMultiOracle.sol: function setSources(bytes6[] memory bases, bytes6[] memory quotes, address[] memory sources_) public auth {
oracles\compound\CompoundMultiOracle.sol: function setSource(bytes6 base, bytes6 kind, address source) public auth {
oracles\compound\CompoundMultiOracle.sol: function setSources(bytes6[] memory bases, bytes6[] memory kinds, address[] memory sources_) public auth {
oracles\uniswap\UniswapV3Oracle.sol: function setSecondsAgo(uint32 secondsAgo_) public auth {
oracles\uniswap\UniswapV3Oracle.sol: function setSource(bytes6 base, bytes6 quote, address source) public auth {
oracles\uniswap\UniswapV3Oracle.sol: function setSources(bytes6[] memory bases, bytes6[] memory quotes, address[] memory sources_) public auth {
fytoken.sol: function setOracle(IOracle oracle_) public auth {

Tools Used

grep

Recommended Mitigation Steps

make sure all auth functions use external (still error prone)
or change the modifier to something like:

modifier auth(bytes4 fs) {
require (msg.sig == fs,"Wrong selector");
require (_hasRole(msg.sig, msg.sender), "Access denied");
_;
}

function setFee(uint256) public auth(this.setFee.selector) {
   .....
}

auth collision possible

Handle

gpersoon

Vulnerability details

Impact

The auth mechanism of AccessControl.sol uses function selectors (msg.sig) as a (unique) role definition.
Also the _moduleCall allows the code to be extended.
Suppose an attacker wants to add the innocent looking function "left_branch_block(uint32)" in an new module.
Suppose this module is added via _moduleCall and the attacker gets authorization for the innocent function.
This functions happens to have a signature of 0x00000000, which is equal to the root authorization.
This way the attacker could get authorization for the entire project.

Note: it's pretty straightforward to generate function names for any signature value, you can just brute force it because it's only 4 bytes.

Proof of Concept

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/access/AccessControl.sol#L90
modifier auth() {
require (_hasRole(msg.sig, msg.sender), "Access denied");
_;
}

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Ladle.sol#L588
function _moduleCall(address module, bytes memory moduleCall)
private
returns (bool success, bytes memory result)
{
require (modules[module], "Unregistered module");
(success, result) = module.delegatecall(moduleCall);
if (!success) revert(RevertMsgExtractor.getRevertMsg(result));
}
}

// https://www.4byte.directory/signatures/?bytes4_signature=0x00000000
Text Signature Bytes Signature
left_branch_block(uint32) 0x00000000

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/access/AccessControl.sol#L47
bytes4 public constant ROOT = 0x00000000;

Tools Used

Recommended Mitigation Steps

Do not allow third parties to define or suggest new modules
Double check the function signatures of new functions of a new module for collisions

Prevent the use of LOCK in setRoleAdmin to instead force the use of lockRole

Handle

0xRajeev

Vulnerability details

Impact

The LOCK role is special in AccessControl because it has itself as the admin role (like ROOT) but no members. This means that calling setRoleAdmin(msg.sig, LOCK) means no one can grant/revoke that msg.sig role anymore and it gets locked irreversibly. This means it disables admin-based permissioning management of that role and therefore is very powerful in its impact.

Given this, there is a special function lockRole() which is specifically meant to enforce LOCK as the admin for the specified role parameter. For all other role admin creations, the generic setRoleAdmin() may be used. However, setRoleAdmin() does not itself prevent specifying the use of LOCK as the admin. If this is accidentally used then it leads to disabling that role’s admin management irreversibly similar to the lockRole() function.

It is safer to force admins to use lockRole() as the only way to set admin to LOCK and prevent the use of LOCK as the adminRole parameter in setRoleAdmin(), because doing so will make the intention of the caller clearer as lockRole() clearly has that functionality specified in its name and that’s the only thing it does.

Proof of Concept

Alice who is the admin for foo() wants to give the admin rights to Bob (0xFFFFFFF0) but instead of calling setRoleAdmin(foo.sig, 0xFFFFFFF0), she calls setRoleAdmin(foo.sig, 0xFFFFFFFF) where 0xFFFFFFFF is LOCK. This makes LOCK as the admin for foo() and prevents any further admin-based access control management for foo().

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L48

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L129-L131

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L235-L240

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L165-L176

Tools Used

Manual Analysis

Recommended Mitigation Steps

Prevent the use of LOCK as the adminRole parameter in setRoleAdmin().

_burnInternal always returns 0 for fy tokens returned

Handle

pauliax

Vulnerability details

Impact

Function _burnInternal always returns 0 as a third parameter. It should return tokensBurnt, tokenOut, fyTokenOut.

Recommended Mitigation Steps

return (tokensBurned, tokenOut, fyTokenOut);

YieldMath.sol / Log2: >= or > ?

Handle

gpersoon

Vulnerability details

Impact

The V1 version of YieldMath.sol contains ">=" (larger or equal), while the V2 version of YieldMath.sol containt ">" (larger) in the log_2 function.
This change doesn't seem logical and might lead to miss calculations.
The difference is present in a number of adjacent lines.

Proof of Concept

// https://github.com/yieldprotocol/yieldspace-v1/blob/master/contracts/YieldMath.sol#L217
function log_2 (uint128 x)
...
b = b * b >> 127; if (b >= 0x100000000000000000000000000000000) {b >>= 1; l |= 0x1000000000000000000000000000000;}

//https://github.com/code-423n4/2021-05-yield/blob/main/contracts/yieldspace/YieldMath.sol#L58
function log_2(uint128 x)
...
b = b * b >> 127; if(b > 0x100000000000000000000000000000000) {b >>= 1; l |= 0x1000000000000000000000000000000;}

Tools Used

diff

Recommended Mitigation Steps

Check which version is the correct version and fix the incorrect version.

Potential griefing with DoS by front-running vault creation with same vaultID

Handle

0xRajeev

Vulnerability details

Impact

The vaultID for a new vault being built is required to be specified by the user building a vault via the build() function (instead of being assigned by the Cauldron/protocol). An attacker can observe a build() as part of a batch transaction in the mempool, identify the vaultID being requested and front-run that by constructing a malicious batch transaction with only the build operation with that same vaultID. The protocol would create a vault with that vaultID and assign attacker as its owner. More importantly, the valid batch transaction in the mempool which was front-run will later fail to create its vault because that vaultID already exists, as per the check on Line180 of Cauldron.sol. As a result, the valid batch transaction fails entirely because of the attacker front-running with the observed vaultID.

While the attacker gains nothing except the ownership of an empty vault after spending the gas, this could grief the protocol’s real users by preventing them from opening a vault and interacting with the protocol in any manner.

Rationale for Medium-severity impact: While the likelihood of this may be low, the impact is high because valid vaults from the Yield front-end will never be successfully created and will lead to a DoS against the entire protocol’s functioning. So, with low likelihood and high impact, the severity (according to OWASP) is medium.

Proof of Concept

Alice uses Yield’s front-end to create a valid batch transaction. Evil Eve observes that in the mempool and identifies the vaultID of the vault being built by Alice. Eve submits her own batch transaction (without using the front-end) with only a build operation using Alice’s vaultID. She uses a higher gas price to front-run Alice’s transaction and get’s the protocol to assign that vaultID to herself. Alice’s batch transaction later fails because the vaultID she requested is already assigned to Eve. Eve can do this for any valid transaction to grief protocol users by wasting her gas to cause DoS.

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L180

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L173-L190

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L133-L135

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L249-L255

Tools Used

Manual Analysis

Recommended Mitigation Steps

Mitigate this DoS vector by having the Cauldron assign the vauldID instead of user specifying it in the build() operation. This would likely require the build() to be a separate non-batch transaction followed by other operations that use the vaultID assigned in build(). Consider the pros/cons of this approach because it will significantly affect the batching/caching logic in Ladle.

Alternatively, consider adding validation logic in Ladle’s batching to revert batches that have only build or a subset of the operations that do not make sense to the protocol’s operations per valid recipes, which could be an attacker’s signature pattern.

Unlocked Pragma

Handle

cmichel

Vulnerability details

Contracts should be deployed using the same compiler version/flags with which they have been tested. Locking the floating pragma, i.e. by not using ^ in pragma solidity ^0.8.0, ensures that contracts do not accidentally get deployed using an older compiler version with unfixed bugs.

For reference, see https://swcregistry.io/docs/SWC-103

Recommend removing ^ in pragma solidity ^0.8.0 and change it to pragma solidity 0.8.3 to be consistent with the rest of the contracts.

Duplication of Balance

Handle

0xsomeone

Vulnerability details

Impact

It is possible to duplicate currently held ink or art within a Cauldron, thereby breaking the contract's accounting system minting units out of thin air.

Proof of Concept

The stir function of the Cauldron, which can be invoked via a Ladle operation, caches balances in memory before decrementing and incrementing. As a result, if a transfer to self is performed, the assignment balances[to] = balancesTo will contain the added-to balance instead of the neutral balance.

This allows one to duplicate any number of ink or art units at will, thereby severely affecting the protocol's integrity. A similar attack was exploited in the third bZx hack resulting in a roughly 8 million loss.

Code Referenced: https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L268-L295

Tools Used

Manual Review.

Recommended Mitigation Steps

A require check should be imposed that prohibits the from and to variables to be equivalent.

no need for transferToPool to be payable

Handle

pauliax

Vulnerability details

Impact

function transferToPool is marked as 'payable'. It only transfers ERC20 tokens, no Ether, so there is no need in having 'payable' here.

Recommended Mitigation Steps

Remove 'payable' modifier from function transferToPool.

Several todos left in the code

Handle

gpersoon

Vulnerability details

Impact

The code still has some todos, which should be resolved before production

Proof of Concept

Ladle.sol: weth.deposit{ value: ethTransferred }(); // TODO: Test gas savings using WETH10 depositTo
Ladle.sol: weth.withdraw(ethTransferred); // TODO: Test gas savings using WETH10 withdrawTo
Wand.sol: cauldron.setRateOracle(assetId, IOracle(address(oracle))); // TODO: Consider adding a registry of chi oracles in cauldron as well
Wand.sol: ); // TODO: Use a FYTokenFactory to make Wand deployable at 20000 runs
Wand.sol: name, // Derive from base and maturity, perhaps
Wand.sol: symbol // Derive from base and maturity, perhaps

Tools Used

Grep

Recommended Mitigation Steps

Check and fix or remove the todos

Useless 'auth' modifier in setSources

Handle

pauliax

Vulnerability details

Impact

function setSources in Oracle contracts does not need 'auth' modifier as it will be checked anyway in function setSource. This does not impact the security, it is just a useless check that can be removed.

Recommended Mitigation Steps

Remove 'auth' modifer from function setSources.

Unauthorized functions in Ladle.sol and PoolRouter.sol

Handle

gpersoon

Vulnerability details

Impact

Both Ladle.sol and PoolRouter.sol contain a function batch, which gives access to several internal functions.
Some of those functions call functions in other contracts which have an "auth" access control mechanism.
However several internal functions can just be executed without any additional checks. These include the functions:
// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Ladle.sol
_retrieve, _forwardPermit, _forwardDaiPermit, _joinEther, _exitEther, _transferToPool, _route, _transferToFYToken, _redeem, _moduleCall
// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/yieldspace/PoolRouter.sol#L162
_route._transferToPool._forwardPermit._forwardDaiPermit._joinEther.

The most risky functions seem to be: _redeem, _exitEther and _moduleCall
_redeem and _exitEther allow the transfer of tokens and eth out of the Ladle.sol and PoolRouter.sol contract.
_moduleCall allows for arbitrary calls to external modules.

Proof of Concept

https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Ladle.sol#L539
function batch(Operation[] calldata operations, bytes[] calldata data) external payable {
...
_exitEther(payable(to));

function _exitEther(address payable to) private returns (uint256 ethTransferred)
{
ethTransferred = weth.balanceOf(address(this));
weth.withdraw(ethTransferred); // TODO: Test gas savings using WETH10 withdrawTo
to.safeTransferETH(ethTransferred);
}

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/yieldspace/PoolRouter.sol#L162
function batch( PoolDataTypes.Operation[] calldata operations,bytes[] calldata data) external payable {
....
_exitEther(to);

function _exitEther(address to) private returns (uint256 ethTransferred)  {
    ethTransferred = weth.balanceOf(address(this));
    weth.withdraw(ethTransferred);   // TODO: Test gas savings using WETH10 `withdrawTo`
    payable(to).safeTransferETH(ethTransferred);
}

}

Tools Used

Recommended Mitigation Steps

Also authorize the functions called via the batch function, especially _redeem, _exitEther and _moduleCall

Uninitialized or Incorrectly set auctionInterval may lead to liquidation engine livelock

Handle

0xRajeev

Vulnerability details

Impact

The grab() function in Cauldron is used by the Witch or other liquidation engines to grab vaults that are under-collateralized. To prevent re-grabbing without sufficient time for auctioning collateral/debt, the logic uses an auctionInterval threshold to give a reasonable window to a liquidation engine that has grabbed the vault.

The grab() function has a comment on Line 354: “// Grabbing a vault protects it for a day from being grabbed by another liquidator. All grabbed vaults will be suddenly released on the 7th of February 2106, at 06:28:16 GMT. I can live with that.” indicating a requirement of the auctionInterval being equal to one day. This can happen only if the auctionInterval is set appropriately. However, this state variable is uninitialized (defaults to 0) and depends on setAuctionInterval() being called with the appropriate auctionInterval_ value which is also not validated.

Discussion with the project lead indicated that this comment is incorrect. Nevertheless, it is safer to initialize auctionInterval at declaration to a safe default value instead of the current 0 which will allow liquidation engines to re-grab vaults without making any progress on liquidation auction. It is also good to add a threshold check in setAuctionInterval() to ensure the new value meets/exceeds a reasonable default value.

Rationale for Medium-severity impact: While the likelihood of this may be low, the impact is high because liquidation engines will keep re-grabbing vaults from each other and potentially result in liquidation bots entering a live-lock situation without making any progress on liquidation auctions. This will result in collateral being stuck and impact entire protocol’s functioning. So, with low likelihood and high impact, the severity (according to OWASP) is medium.

Proof of Concept

Configuration recipe forgets to set the auctionInterval state variable by calling setAuctionInterval() and inadvertently leaves it at the default value of 0. Alternatively, it calls it but with a lower than intended/reasonable auction interval value. Both scenarios fail to give sufficient protection to liquidation engines from having their grabbed vaults re-grabbed without sufficient time for liquidation auctions.

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L63

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L108-L115

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L354

Tools Used

Manual Analysis

Recommended Mitigation Steps

  1. Initialize auctionInterval at declaration with a reasonable default value.
  2. Add a threshold check in setAuctionInterval() to ensure the new value meets/exceeds a reasonable default value.

Violation of implicit constraints in batched operations may break protocol assumptions

Handle

0xRajeev

Vulnerability details

Impact

The Ladle batching of operations is a complex task (as noted by the project lead) which has implicit constraints on what operations can be bundled together in a batch, which operations can/have-to appear how many times and in what order/sequence etc. Some examples of these constraints are: Join Ether should be the first operation, Exit Ether the last, and only one Join Ether per batch.

All this complexity is managed currently by anticipating all interactions to happen via their authorised front-end which uses validated (and currently revealed only on demand) recipes that adhere to these constraints. There is a plan to open the design up to other front-ends and partner integrating protocols who will also test their batch recipes or integrations for these constraints.

Breaking some of these constraints opens up the protocol to failing transactions, undefined behaviour or potentially loss/lock of funds. Defensive programming suggests enforcing such batch operation constraints in the code itself along with documentation and onboarding checks for defense-in-depth. Relying on documentation or external validation may not be sufficient for arguably the most critical aspect of batched operations which is the only authorized way to interact with the protocol.

Rationale for Medium-severity impact: While the likelihood of this may be low because of controlled/validated onboarding on new front-ends or integrating protocols, the impact of accidental deviation from implicit constraints is high. This may result in transaction failing or tokens getting locked/lost, thus impact the entire protocol’s functioning. So, with low likelihood and high impact, the severity (according to OWASP) is medium.

Proof of Concept

  1. A new front-end project comes up claiming to provide a better user-interface than the project’s authorised front-end. It does not use the recipe book (correctly) and ends up making Ladle batches with incorrect operations which fail the constraints leading to protocol failures and token lock/loss.

  2. An integrating protocol goes through the approved onboarding and validation but has missed bugs in its recipe for batches which fail the constraints leading to protocol failures and token lock/loss.

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L120-L245

Comment on Join Ether constraint: https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L525

Comment on Exit Ether constraint: https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L538

Tools Used

Manual Analysis

Recommended Mitigation Steps

Enforce batch operation constraints explicitly in the code (e.g. with tracking counters/booleans for operations) along with documentation and onboarding validation. This may increase the complexity of the batching code but adds fail-safe defense-in-depth for any mistakes in onboarding validation of implicit constraints which may affect protocol operations significantly.

Join Factory Contract Replacement

Handle

0xsomeone

Vulnerability details

Impact

The JoinFactory contract is utilizing the create2 OPCODE (via syntactic sugar) to deploy a new Join instance, however, no sanitization occurs on the inputs allowing contracts and thereby ownerships to be replaced at will.

Proof of Concept

If the createJoin function is invoked with the same asset, it will replace any existing Join in the specified address with a new instance whose ownership will be transferred to the caller. This breaks the logical assumption that ownership of a pool should be retained and dictated by the currently-active owner of the contract.

Referenced Code: https://github.com/code-423n4/2021-05-yield/blob/main/contracts/JoinFactory.sol#L64-L75

Tools Used

Manual Review.

Recommended Mitigation Steps

A require check should be imposed prohibiting deployments of already-created Joins by either utilizing a mapping for the hashes that is set to true or by dynamically evaluating whether a contract already exists at the specified address via an isContract invocation.

TEST ISSUE

Handle

0xRajeev

Vulnerability details

Impact

Detailed description of the impact of this finding.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

Different solidity pramas

Handle

gpersoon

Vulnerability details

Impact

The code contains several different solidity pragmas, indicating different versions.
Its cleaner and safer to use the same version everywhere, preferably a fixed version

Proof of Concept

.\Cauldron.sol:pragma solidity ^0.8.0;
.\FYToken.sol:pragma solidity ^0.8.0;
.\Join.sol:pragma solidity ^0.8.0;
.\JoinFactory.sol:pragma solidity >= 0.8.0;
.\Ladle.sol:pragma solidity ^0.8.0;
.\LadleStorage.sol:pragma solidity ^0.8.0;
.\Wand.sol:pragma solidity ^0.8.0;
.\Witch.sol:pragma solidity ^0.8.0;
.\interfaces\external\IERC20.sol:pragma solidity ^0.8.0;
.\interfaces\external\IERC20Metadata.sol:pragma solidity ^0.8.0;
.\interfaces\external\IERC2612.sol:pragma solidity ^0.8.0;
.\interfaces\external\IWETH9.sol:pragma solidity ^0.8.0;
.\interfaces\vault\DataTypes.sol:pragma solidity ^0.8.0;
.\interfaces\vault\ICauldron.sol:pragma solidity ^0.8.0;
.\interfaces\vault\ICauldronGov.sol:pragma solidity ^0.8.0;
.\interfaces\vault\IFYToken.sol:pragma solidity ^0.8.0;
.\interfaces\vault\IJoin.sol:pragma solidity ^0.8.0;
.\interfaces\vault\IJoinFactory.sol:pragma solidity >= 0.8.0;
.\interfaces\vault\ILadle.sol:pragma solidity ^0.8.0;
.\interfaces\vault\ILadleGov.sol:pragma solidity ^0.8.0;
.\interfaces\vault\IMultiOracleGov.sol:pragma solidity ^0.8.0;
.\interfaces\vault\IOracle.sol:pragma solidity ^0.8.0;
.\interfaces\yieldspace\IPool.sol:pragma solidity >= 0.8.0;
.\interfaces\yieldspace\IPoolFactory.sol:pragma solidity >= 0.8.0;
.\interfaces\yieldspace\PoolDataTypes.sol:pragma solidity >= 0.8.0;
.\math\CastBytes32Bytes6.sol:pragma solidity ^0.8.0;
.\math\CastI128U128.sol:pragma solidity ^0.8.0;
.\math\CastU128I128.sol:pragma solidity ^0.8.0;
.\math\CastU256I256.sol:pragma solidity ^0.8.0;
.\math\CastU256U128.sol:pragma solidity ^0.8.0;
.\math\CastU256U32.sol:pragma solidity ^0.8.0;
.\math\WDiv.sol:pragma solidity ^0.8.0;
.\math\WDivUp.sol:pragma solidity ^0.8.0;
.\math\WMul.sol:pragma solidity ^0.8.0;
.\math\WMulUp.sol:pragma solidity ^0.8.0;
.\oracles\chainlink\AggregatorV3Interface.sol:pragma solidity >=0.6.0;
.\oracles\chainlink\ChainlinkMultiOracle.sol:pragma solidity ^0.8.0;
.\oracles\compound\CompoundMultiOracle.sol:pragma solidity ^0.8.0;
.\oracles\compound\CTokenInterface.sol:pragma solidity >=0.5.16;
.\oracles\uniswap\IUniswapV3PoolImmutables.sol:pragma solidity >=0.5.0;
.\oracles\uniswap\UniswapV3Oracle.sol:pragma solidity ^0.8.0;
.\utils\AddressStringUtil.sol:pragma solidity >=0.5.0;
.\utils\RevertMsgExtractor.sol:pragma solidity >=0.6.0;
.\utils\access\AccessControl.sol:pragma solidity ^0.8.0;
.\utils\access\Ownable.sol:pragma solidity ^0.8.0;
.\utils\token\ERC20.sol:pragma solidity ^0.8.0;
.\utils\token\ERC20Permit.sol:pragma solidity ^0.8.0;
.\utils\token\MinimalTransferHelper.sol:pragma solidity >=0.6.0;
.\utils\token\SafeERC20Namer.sol:pragma solidity >=0.5.0;
.\utils\token\TransferHelper.sol:pragma solidity >=0.6.0;
.\yieldspace\Math64x64.sol:pragma solidity >= 0.8.0;
.\yieldspace\Pool.sol:pragma solidity >= 0.8.0;
.\yieldspace\PoolFactory.sol:pragma solidity >= 0.8.0;
.\yieldspace\PoolRouter.sol:pragma solidity >= 0.8.0;
.\yieldspace\YieldMath.sol:pragma solidity >= 0.8.0;

Tools Used

grep

Recommended Mitigation Steps

Use the same version everywhere, for example:
pragma solidity 0.8.4

Missing checks on debt max/min limits could cause pour to revert

Handle

0xRajeev

Vulnerability details

Impact

setDebtLimits() is used to set the maximum and minimum debt for an underlying and ilk pair. The assumption is that max will be greater than min while setting them because otherwise the debt checks in _pour() for line/dust will fail and revert.

While max and min debt limits can be reset, it is safer to perform input validation on them in setDebtLimits().

Proof of Concept

A recipe incorrectly interchanges the values of min and max debt which leads to exceptions in pouring into the vaults.

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L91-L92

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L319-L322

https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Wand.sol#L79

Tools Used

Manual Analysis

Recommended Mitigation Steps

Add a check to ensure max > mix.

Witch can't give back vault after 2x grab

Handle

gpersoon

Vulnerability details

Impact

The witch.sol contract gets access to a vault via the grab function, in case of liquidation.
If the witch.sol contract can't sell the debt within a certain amount of time, a second grab can occur.

After the second grab, the information of the original owner of the vault is lost and the vault can't be returned to the original owner once the debt has been sold.

The grab function stores the previous owner in vaultOwners[vaultId] and then the contract itself is the new owner (via cauldron.grab and cauldron._give).
The vaultOwners[vaultId] is overwritten at the second grab

The function buy of Witch.sol tried to give the vault back to the original owner, which won't succeed after a second grab.

Proof of Concept

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Witch.sol#L50
function grab(bytes12 vaultId) public {
DataTypes.Vault memory vault = cauldron.vaults(vaultId);
vaultOwners[vaultId] = vault.owner;
cauldron.grab(vaultId, address(this));
}

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L349
function grab(bytes12 vaultId, address receiver) external auth {
...
_give(vaultId, receiver);

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L349
function _give(bytes12 vaultId, address receiver) internal returns(DataTypes.Vault memory vault) {
...
vault.owner = receiver;
vaults[vaultId] = vault;

// https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Witch.sol#L57
function buy(bytes12 vaultId, uint128 art, uint128 min) public {
....
cauldron.give(vaultId, vaultOwners[vaultId]);

Tools Used

Editor

Recommended Mitigation Steps

Assuming it's useful to give back to vault to the original owner:
Make a stack/array of previous owners if multiple instances of the witch.sol contract would be used.
Or check if the witch is already the owner (in the grab function) and keep the vaultOwners[vaultId] if that is the case

Uniswap Oracle uses wrong prices

Handle

cmichel

Vulnerability details

The Uniswap oracle uses a mock contract with hard-coded prices to retrieve the price which is not feasible in production.
Not sure if this is part of the contest, this will probably still be changed? But note that even when using the "real deal" @uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol it does not return the prices.

Impact

The price could change from the set price and always updating new prices with set will be too slow and gas expensive.

Recommended Mitigation Steps

Use cumulativeTicks = pool.observe([secondsAgo, 0]) // [a_t1, a_t2] and apply equation 5.5 from the Uniswap V3 whitepaper to compute the token0 TWAP.
Note that even the official .consult call seems to only return the averaged cumulative ticks, you'd still need to compute the 1.0001^timeWeightedAverageTick in the function.

UniswapV3Oracle function _peek is public

Handle

pauliax

Vulnerability details

Impact

In contract UniswapV3Oracle function _peek has visibility of public while the name and similar functions in other oracles are declared as private.

Recommended Mitigation Steps

give _peek private visibility.

`FlashBorrower` uses non-safe ERC20 functions

Handle

cmichel

Vulnerability details

The FlashBorrower uses non-save ERC20 functions like .transfer(From) and approve.

The ERC20.transfer() and ERC20.transferFrom() functions return a boolean value indicating success. This parameter needs to be checked for success.
Furthermore, some tokens (like USDT) don't correctly implement the ERC20 standard and don't return a boolean.

Impact

Tokens that don't actually perform the transfer and return false are still counted as a correct transfer.
Tokens that don't correctly implement the spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value.

Recommended Mitigation Steps

Use OpenZeppelin's SafeERC20 library or the custom implementation already present in Utils/transferHelper.sol.

Mining

Handle

s1m0

Vulnerability details

Impact

Detailed description of the impact of this finding.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Recommended Mitigation Steps

setDebtLimits should check that max >= min

Handle

pauliax

Vulnerability details

Impact

function setDebtLimits should explicitly enforce that parameters max >= min. It is not a vulnerability but I think it is a good practice to enforce such validation in code.

Recommended Mitigation Steps

require(max >= min, "max < min");

Unsafe call to `.decimals`

Handle

cmichel

Vulnerability details

The FYToken.constructor performs an external call to IERC20Metadata(address(IJoin(join_).asset())).decimals().
This function was optional in the initial ERC-20 and might fail for old tokens that therefore did not implement it.

Impact

FyTokens cannot be created for tokens that implemented the old initial ERC20 without the decimals function.

Recommended Mitigation Steps

Consider using the helper function in the utils to retrieve it SafeERC20Namer.tokenDecimals, the same way the Pool.constructor works.

Vaults are in liquidation forever instead of just for auction length

Handle

cmichel

Vulnerability details

The witch can Witch.grab vaults and the vaultOwners[vaultId] field is set to the original owner.
The original vault owner is only restored if all debt (balances_.art) is repaid by the liquidation engine.

if (balances_.art - art == 0) { // If there is no debt left, return the vault with the collateral to the owner
    cauldron.give(vaultId, vaultOwners[vaultId]);
    delete vaultOwners[vaultId];
}

Note that there's no check in settle verifying that the auction time (from grab) is not over yet, as well as no check that the vault is actually still undercollateralized.

Impact

Once a vault is grabbed by the witch it'll be susceptible to liquidations forever. All debt has to be repaid to get the vault out of a liquidation state again.
An example would be that a vault becomes undercollateralized, the witch grabs it, the network is congested and nobody is able to liquidate it, the auction time is over, the collateral value has increased in the meantime and the vault is not undercollateralized anymore.
Liquidators can still liquidate this vault whenever they want which doesn't seem fair to the vault owner.

Recommended Mitigation Steps

Liquidations should only occur during the auction time. If settle is called after auction time (maybe with a small buffer to give liquidators the chance to fully liquidate all collateral at elapsed >= auctionTime), it should restore the original owner and it must be grabbed again by the witch (this also performs a collateralization level check again in grab, which is good).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.