Platform: Code4rena
Start Date: 21/07/2023
Pot Size: $90,500 USDC
Total HM: 8
Participants: 60
Period: 7 days
Judge: 0xean
Total Solo HM: 2
Id: 264
League: ETH
Rank: 15/60
Findings: 1
Award: $613.47
π Selected for report: 1
π Solo Findings: 0
π Selected for report: Juntao
Also found by: Jiamin, Juntao, UniversalCrypto, auditsea, circlelooper, crunch, lanrebayode77, vangrim, zaevlad
613.4664 USDC - $613.47
Approved gscApprove allowance to an address may not able to be decreased.
GSC_CORE_VOTING_ROLE calls gscApprove(...) function to approve tokens to be pulled from the treasury:
function gscApprove( address token, address spender, uint256 amount ) external onlyRole(GSC_CORE_VOTING_ROLE) nonReentrant { if (spender == address(0)) revert T_ZeroAddress("spender"); if (amount == 0) revert T_ZeroAmount(); // Will underflow if amount is greater than remaining allowance gscAllowance[token] -= amount; _approve(token, spender, amount, spendThresholds[token].small); }
Each approval will decrease the gscAllowance[token]
by the approved allowance amount, this is problematic as GSC_CORE_VOTING_ROLE may not able to decrease the approved allowance.
Consider the following scenario:
gscAllowance[token]
is 100;gscAllowance[token]
is 40 now;gscAllowance[token]
is less than 50)Please see the tests:
contract ArcadeTreasuryTest is Test { address timelock = address(1); address coreVoting = address(2); address gscCoreVoting = address(3); address other = address(4); ArcadeTreasury treasury; MockToken mockToken; function setUp() public { treasury = new ArcadeTreasury(timelock); vm.startPrank(timelock); treasury.grantRole(treasury.CORE_VOTING_ROLE(), coreVoting); treasury.grantRole(treasury.GSC_CORE_VOTING_ROLE(), gscCoreVoting); mockToken = new MockToken("Mock Token", "MT"); IArcadeTreasury.SpendThreshold memory threshold = IArcadeTreasury.SpendThreshold(100 ether, 200 ether, 300 ether); treasury.setThreshold(address(mockToken), threshold); vm.stopPrank(); } function testGscApprove() public { vm.warp(7 days); vm.prank(timelock); treasury.setGSCAllowance(address(mockToken), 100 ether); vm.startPrank(gscCoreVoting); treasury.gscApprove(address(mockToken), address(4), 60 ether); vm.expectRevert(stdError.arithmeticError); treasury.gscApprove(address(mockToken), address(4), 50 ether); vm.stopPrank(); } } contract MockToken is ERC20 { constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} function mint(address account, uint256 amount) public { _mint(account, amount); } }
Manual Review
When approve to a particular address, compare the new allowance with current allowance (IERC20.allowance(...)):
gscAllowance[token] -= (new allowance - current allowance)
;gscAllowance[token] += (current allowance - new allowance)
.Access Control
#0 - c4-pre-sort
2023-07-29T14:01:41Z
141345 marked the issue as primary issue
#1 - 141345
2023-07-29T14:01:51Z
Already approved allowance can not be decreased.
Might be expected behavior.
Because in this report, the focus is inner accounting gscAllowance, not the external erc20 allownance (as oppose to https://github.com/code-423n4/2023-07-arcade-findings/issues/85). Here the approved allowance seems different from the "ERC20 approved allowance". Here it can be seen as account payable, the amount is spent, but the spender has not taken yet.
Or, if the designed behavior is to also provide way to revoke the allowance, this inner accounting needs some improvement.
To my understanding, the issue roots in the unsync of internal and external allowance accounting.
https://github.com/code-423n4/2023-07-arcade-findings/issues/85 also talks about allownance issue, focus more on erc20 approved allowance override, which seems better pointing out the potential impact of with this mechanism. Some way to revoke the allowance, if the allowance is overwritten.
#2 - 141345
2023-08-01T07:09:12Z
group several gscAllowance issue, with some different impact, but roots from the gscAllowance accounting.
#3 - c4-sponsor
2023-08-02T19:52:28Z
PowVT marked the issue as sponsor confirmed
#4 - c4-sponsor
2023-08-02T19:52:37Z
PowVT marked the issue as disagree with severity
#5 - 0xean
2023-08-10T14:42:37Z
Based on current understanding, I think M is the correct severity here.
#6 - c4-judge
2023-08-10T14:42:51Z
0xean changed the severity to 2 (Med Risk)
#7 - c4-judge
2023-08-11T01:40:23Z
0xean marked issue #288 as primary and marked this issue as a duplicate of 288
#8 - c4-judge
2023-08-11T01:40:32Z
0xean marked the issue as selected for report
π Selected for report: Juntao
Also found by: Jiamin, Juntao, UniversalCrypto, auditsea, circlelooper, crunch, lanrebayode77, vangrim, zaevlad
613.4664 USDC - $613.47
Tokens may be spent without CORE_VOTING_ROLE approval.
ArcadeTreasury transfers or approves them based on invocations from governance. GSC_CORE_VOTING_ROLE may be authorized to spend or approve smaller amounts (gscAllowance
) from their own voting contract, all other amounts must be authorized by full community votes.
ADMIN_ROLE can set the gscAllowance
for a token by calling setGSCAllowance(...) function:
function setGSCAllowance(address token, uint256 newAllowance) external onlyRole(ADMIN_ROLE) { if (token == address(0)) revert T_ZeroAddress("token"); if (newAllowance == 0) revert T_ZeroAmount(); // enforce cool down period if (uint48(block.timestamp) < lastAllowanceSet[token] + SET_ALLOWANCE_COOL_DOWN) { revert T_CoolDownPeriod(block.timestamp, lastAllowanceSet[token] + SET_ALLOWANCE_COOL_DOWN); } uint256 spendLimit = spendThresholds[token].small; // new limit cannot be more than the small threshold if (newAllowance > spendLimit) { revert T_InvalidAllowance(newAllowance, spendLimit); } // update allowance state lastAllowanceSet[token] = uint48(block.timestamp); gscAllowance[token] = newAllowance; emit GSCAllowanceUpdated(token, newAllowance); }
However, part of gscAllowance
may be approved to third parties and the allowance is not reset, thus tokens may be spent without CORE_VOTING_ROLE approval.
Consider the following scenario:
gscAllowance
to 80 ether;Please see the tests:
contract ArcadeTreasuryTest is Test { address timelock = address(1); address coreVoting = address(2); address gscCoreVoting = address(3); address other = address(4); ArcadeTreasury treasury; MockToken mockToken; function setUp() public { treasury = new ArcadeTreasury(timelock); vm.startPrank(timelock); treasury.grantRole(treasury.CORE_VOTING_ROLE(), coreVoting); treasury.grantRole(treasury.GSC_CORE_VOTING_ROLE(), gscCoreVoting); mockToken = new MockToken("Mock Token", "MT"); IArcadeTreasury.SpendThreshold memory threshold = IArcadeTreasury.SpendThreshold(100 ether, 200 ether, 300 ether); treasury.setThreshold(address(mockToken), threshold); vm.stopPrank(); uint256 gscAllowance = 100 ether; mockToken.mint(address(treasury), 300 ether); uint256 treasuryBalanceBefore = mockToken.balanceOf(address(treasury)); assertEq(treasuryBalanceBefore, 300 ether); vm.warp(7 days); vm.prank(timelock); treasury.setGSCAllowance(address(mockToken), gscAllowance); } function testDecreaseGscAllowance() public { vm.prank(gscCoreVoting); treasury.gscApprove(address(mockToken), other, gscAllowance); vm.warp(14 days); uint256 gscAllowanceNew = 80 ether; vm.prank(timelock); treasury.setGSCAllowance(address(mockToken), gscAllowanceNew); vm.prank(other); mockToken.transferFrom(address(treasury), other, gscAllowance); uint256 treasuryBalanceAfter = mockToken.balanceOf(address(treasury)); assertTrue(treasuryBalanceAfter < treasuryBalanceBefore - gscAllowanceNew); assertEq(mockToken.balanceOf(address(treasury)), 200 ether); } } contract MockToken is ERC20 { constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} function mint(address account, uint256 amount) public { _mint(account, amount); } }
Manual Review
Allowance to third parties should be revoked when set the gscAllowance
.
Access Control
#0 - 141345
2023-07-29T15:05:37Z
seems wrong understanding of the mechanism, new allowance are separate from previous allowance.
Already allocated allowance should not be affected.
#1 - c4-pre-sort
2023-07-29T15:07:03Z
141345 marked the issue as duplicate of #480
#2 - 141345
2023-07-29T15:07:31Z
not exactly same as https://github.com/code-423n4/2023-07-arcade-findings/issues/480, but similar
#3 - c4-pre-sort
2023-07-29T16:42:00Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-07-29T16:42:04Z
141345 marked the issue as primary issue
#5 - c4-pre-sort
2023-08-01T07:07:23Z
141345 marked the issue as duplicate of #58
#6 - c4-judge
2023-08-10T14:42:48Z
0xean changed the severity to 2 (Med Risk)
#7 - c4-judge
2023-08-11T01:40:46Z
0xean marked the issue as satisfactory