HardhatDeFi
15,000 USDC
View results
Submission Details
Severity: high
Invalid

Lack of Access Control for Yield Claims

Summary

The function claimYield on line 237 in IAaveDIVAWrapper.sol contract allows the owner of the contract to claim accrued yield, but there may be scenarios where the contract owner could be compromised, leading to unauthorized claims.

Vulnerability Details

  • If the owner’s private key is compromised, the attacker could withdraw all the accrued yield, which might be significant.

  • The function does not include additional access controls or multi-sig mechanisms to ensure the action is authorized.

Impact

  • If the owner is compromised, an attacker can withdraw all the funds (yield).

PoC for Lack of Access Control for Yield Claims

Overview:

The claimYield function in the contract allows the contract owner to claim accrued yield, but without any further restrictions. This PoC demonstrates how an attacker could claim yield if they gain control of the owner’s account (private key).

Actors:

  • Attacker: An unauthorized user trying to claim the yield.

  • Victim: The legitimate contract owner (or whoever has access to the function).

  • Protocol: The contract that manages the yield claims.

Steps:

  1. Deploy Contract: Deploy a contract that implements the claimYield function.

  2. Compromise the Owner: The attacker obtains control over the contract owner's private key.

  3. Claim Yield: The attacker calls claimYield to steal yield.

Working Test Case:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract YieldClaim {
address public owner;
uint256 public yieldAmount;
// Emitted when yield is claimed
event YieldClaimed(address indexed owner, uint256 amount);
constructor() {
owner = msg.sender; // Set the contract deployer as the owner
yieldAmount = 1000; // Initial yield available for claim
}
// Function to simulate yield claiming
function claimYield() external {
require(msg.sender == owner, "Only the owner can claim yield"); // This lacks sufficient checks
uint256 amountToClaim = yieldAmount;
yieldAmount = 0; // Clear the yield once claimed
emit YieldClaimed(msg.sender, amountToClaim);
payable(msg.sender).transfer(amountToClaim); // Transfer the yield
}
// Function to simulate funding the contract (for demonstration purposes)
function fundContract() external payable {
yieldAmount += msg.value;
}
// Fallback function to accept ether
receive() external payable {}
}
  • Test Case to Demonstrate Attack:

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("YieldClaim Contract", function () {
let owner, attacker, contract;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
const YieldClaim = await ethers.getContractFactory("YieldClaim");
contract = await YieldClaim.deploy();
await contract.deployed();
// Fund the contract with 10 ether (simulate yield accumulation)
await contract.fundContract({ value: ethers.utils.parseEther("10") });
});
it("should allow the owner to claim yield", async function () {
const initialOwnerBalance = await ethers.provider.getBalance(owner.address);
// Owner claims yield
await contract.connect(owner).claimYield();
const finalOwnerBalance = await ethers.provider.getBalance(owner.address);
expect(finalOwnerBalance.sub(initialOwnerBalance)).to.equal(ethers.utils.parseEther("10"));
});
it("should allow an attacker to claim yield if they control the owner's private key", async function () {
const initialAttackerBalance = await ethers.provider.getBalance(attacker.address);
// Attacker connects to the contract as the owner and claims yield
await contract.connect(attacker).claimYield();
const finalAttackerBalance = await ethers.provider.getBalance(attacker.address);
expect(finalAttackerBalance.sub(initialAttackerBalance)).to.equal(ethers.utils.parseEther("10"));
});
});

Exploit Scenario:

  1. Initial Setup: The contract is deployed, and the owner funds the contract with 10 ether. The claimYield function allows only the owner to claim the yield.

  2. Compromise: The attacker somehow gains control of the owner's private key (e.g., via a phishing attack, keylogger, or other methods).

  3. Attack: The attacker then connects to the contract as the owner (since they control the owner's private key) and calls claimYield. The attacker successfully withdraws the yield (10 ether) from the contract, even though they are not the legitimate owner.

  4. Outcome: The attacker now holds the yield that should have gone to the legitimate owner.

Outcome & Implications:

  • The attacker can claim the yield intended for the contract owner if they compromise the owner’s private key.

  • This could result in the loss of funds and trust in the protocol.

  • Without proper access control, an attacker can exploit the function by pretending to be the owner.

Recommendations & Fixes:

To mitigate this vulnerability, consider the following:

  • Multi-sig Wallet: Use a multi-sig wallet to require multiple signers before any yield can be claimed.

  • Role-Based Access Control (RBAC): Implement role-based access control using OpenZeppelin’s AccessControl to manage permissions for claiming yield.

  • Delay Mechanisms: Implement a time-lock mechanism where yield claims require a delay, reducing the risk of immediate exploitation after a private key compromise.

For example, using AccessControl from OpenZeppelin:

import "@openzeppelin/contracts/access/AccessControl.sol";
contract YieldClaim is AccessControl {
bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");
constructor() {
_setupRole(OWNER_ROLE, msg.sender); // Set the deployer as the owner
}
function claimYield() external {
require(hasRole(OWNER_ROLE, msg.sender), "Caller is not the owner");
// Yield claiming logic here
}
}

By using the AccessControl contract, only addresses with the OWNER_ROLE can claim the yield, preventing unauthorized users from doing so.

Tools Used

  • Manual code review

Recommendations

  • Consider adding additional layers of access control, such as a multi-sig wallet for yield claim or a time-lock mechanism that requires a delay for sensitive operations.

Updates

Lead Judging Commences

bube Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.