Yield contest - gpersoon's results

Fixed-rate borrowing and lending on Ethereum

General Information

Platform: Code4rena

Start Date: 27/05/2021

Pot Size: $100,000 USDC

Total HM: 12

Participants: 7

Period: 7 days

Judge: LSDan

Total Solo HM: 10

Id: 12

League: ETH

Yield

Findings Distribution

Researcher Performance

Rank: 1/7

Findings: 7

Award: $39,207.69

🌟 Selected for report: 6

🚀 Solo Findings: 3

Findings Information

🌟 Selected for report: 0xsomeone

Also found by: gpersoon

Labels

bug
duplicate
3 (High Risk)
sponsor confirmed

Awards

6443.3912 USDC - $6,443.39

External Links

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

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

#0 - alcueca

2021-06-01T10:21:28Z

Ok, I got spooked with this one. This is a very serious issue, and we are most grateful that you found it.

#1 - uivlis

2021-06-09T12:28:55Z

#2 - dmvt

2021-06-14T20:50:57Z

duplicate of #16

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
3 (High Risk)
sponsor acknowledged
disagree with severity

Awards

14318.6472 USDC - $14,318.65

External Links

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

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

#0 - alcueca

2021-06-01T11:04:20Z

The execution of any auth function will only happen after a governance process, or by a contract that has gone through a thorough review and governance process.

We are aware that new modules can have complete control of the Ladle, and for that reason the addition of new modules would be subject to the highest level of scrutiny. Checking for signature collisions is a good item to add to that process.

In addition to that, I would implement two changes in AccessControl.sol so that giving ROOT access is explicit.

function grantRole(bytes4 role, address account) external virtual admin(role) { require(role != ROOT, "Not ROOT role"); _grantRole(role, account); } function grantRoot(address account) external virtual admin(ROOT) { _grantRole(ROOT, account); }

However, given that this could be exploited only through a malicious governance exploit, I would reduce the risk to "Low"

#1 - alcueca

2021-06-21T12:32:35Z

After further thinking, instead of preventing auth collisions in the smart contracts, we will add CI checks for this specific issue instead.

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
3 (High Risk)
sponsor confirmed
disagree with severity

Awards

14318.6472 USDC - $14,318.65

External Links

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

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

#0 - alcueca

2021-06-01T11:10:57Z

That's entirely my fault, and this is a scary one. We might be having a slightly different or impredictable curve in Pool.sol, and we might notice only after a long while with the Pools being slowly drained. We might never even have found this was the issue.

I would suggest increasing the severity of this issue to High.

#1 - alcueca

2021-06-04T11:07:32Z

Findings Information

🌟 Selected for report: gpersoon

Also found by: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1933.0174 USDC - $1,933.02

External Links

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

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

#0 - alcueca

2021-06-01T10:20:33Z

This is a good finding, and a vulnerability that we will fix. I anticipate that we will store the original owner in Cauldron.auctions along with the time at which the auction was started.

#1 - alcueca

2021-06-02T21:47:44Z

Actually, we might remove some overengineering by taking out the feature of allowing multiple competing liquidation engines.

That would allow us to:

  1. Move the auctions mapping to the Witch
  2. Remove the auctionInterval setting
  3. Simplify grab
  4. Stop worrying about more than one grab

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
2 (Med Risk)
sponsor confirmed
disagree with severity

Awards

1933.0174 USDC - $1,933.02

External Links

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

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) { ..... }

#0 - alcueca

2021-06-01T11:17:30Z

While a number of governance functions have been marked public instead of external throughout the code, they are never called from any other function in the same contract, and they should never be.

In all contracts of the protocol, auth functions are only called by other contracts or by EOAs, with the latter being governance actions.

The suggestion of changing all public auth functions to external auth will be applied, however no changes will be made to AccessControl.sol, and the suggested severity is 1.

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0 USDC - $0.00

External Links

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

See proof of concept above for improved version

Findings Information

🌟 Selected for report: gpersoon

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

0 USDC - $0.00

External Links

Handle

gpersoon

Vulnerability details

Impact

In the function batch of Ladle.sol, at the operation GIVE, the value of vault is stored and is deleted directly afterwards. So storing is unnecessary. Maybe the solidity compiler already optimizes this.

Proof of Concept

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

function batch( } else if (operation == Operation.GIVE) { ... vault = _give(vaultId, to); delete vault; // Clear the cache, since the vault doesn't necessarily belong to msg.sender anymore

Tools Used

Remove the " vault = "

#0 - alcueca

2021-06-02T15:22:06Z

Thanks!

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter