Platform: Code4rena
Start Date: 08/09/2023
Pot Size: $70,000 USDC
Total HM: 8
Participants: 84
Period: 6 days
Judge: gzeon
Total Solo HM: 2
Id: 285
League: ETH
Rank: 29/84
Findings: 2
Award: $296.01
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Kow
Also found by: 0xRobsol, 0xfuje, 0xkazim, 0xpiken, Aymen0909, T1MOH, bin2chen, codegpt, gumgumzum, josephdara, lsaudit, nmirchev8, ravikiranweb3, rvierdiiev
132.8565 USDC - $132.86
https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/token/ERC20.sol#L48
Due to wrong calculation of _DOMAIN_SEPARATOR
, ERC20.permit()
may not verify signature signed by signer. The design is supposed to support offline token approving but can not work as expected.
When calculating _DOMAIN_SEPARATOR
during tranche token deployment, the field name
is also included, but the value is not set in constructor()
. It only has correct value when ERC20.file("name", name)
is called.
From the user's perspective, they will create signature following EIP-2612, the name of the tranche token will be used for _DOMAIN_SEPARATOR
calculation and the result will be different from the value stored in tranche token smart contract, ERC20.permit()
will always be reverted because of this incorrect calculation.
Manual review
Pass name
in ERC20.constructor()
other than assigning it's value through ERC20.file("name", name)
:
constructor(string memory _name, string memory _symbol, uint8 decimals_) { name = _name, symbol = _symbol, decimals = decimals_; wards[_msgSender()] = 1; emit Rely(_msgSender()); deploymentChainId = block.chainid; _DOMAIN_SEPARATOR = _calculateDomainSeparator(block.chainid); }
Other
#0 - c4-pre-sort
2023-09-15T07:09:37Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T07:09:54Z
raymondfam marked the issue as duplicate of #146
#2 - c4-judge
2023-09-26T18:08:09Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: Kow
Also found by: 0xRobsol, 0xfuje, 0xkazim, 0xpiken, Aymen0909, T1MOH, bin2chen, codegpt, gumgumzum, josephdara, lsaudit, nmirchev8, ravikiranweb3, rvierdiiev
132.8565 USDC - $132.86
https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/PoolManager.sol#L214-L225 https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/token/ERC20.sol#L216-L237
ERC20.sol#permit()
uses immutable _DOMAIN_SEPARATOR
to verify signed message unless block.chainid
is changed. If PoolManager#UpdateTrancheTokenMetadata()
is called, ERC20.name
will be modified. Any signed message thereafter will use new value of name
, but the old value is still been used in ERC20.sol#permit()
and cause it reverted.
https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/token/ERC20.sol#L228
We can see that domain separator is recalculated only after block.chainid
changed. The immutable _DOMAIN_SEPARATOR
will be used no mater how name
is modified. Any signed message calculating with new value of name
will be identified as invalid message.
Manual review
Recalculate domain separator when name changed:
contract ERC20 is Context { //@audit-info use to store initial hash of name bytes32 private immutable nameHash; ... constructor(uint8 decimals_) { ... _DOMAIN_SEPARATOR = _calculateDomainSeparator(block.chainid); //@audit-info calculate initial hash of name nameHash = keccak256(bytes(name)); } function permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature) public { ... //@audit-info recalculate domain separator when name changed bytes32 digest = keccak256( abi.encodePacked( "\x19\x01", (block.chainid == deploymentChainId && nameHash == keccak256(bytes(name))) ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid), keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)) ) );] ... } }
Other
#0 - c4-pre-sort
2023-09-15T21:50:57Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T21:51:09Z
raymondfam marked the issue as duplicate of #146
#2 - c4-judge
2023-09-26T18:07:26Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: castle_chain
Also found by: 0xAadi, 0xHelium, 0xLook, 0xblackskull, 0xfuje, 0xmystery, 0xnev, 0xpiken, 7ashraf, BARW, Bauchibred, Bughunter101, Ch_301, JP_Courses, Kaysoft, Krace, MohammedRizwan, SanketKogekar, Sathish9098, alexzoid, ast3ros, btk, catellatech, degensec, fatherOfBlocks, grearlake, imtybik, jkoppel, jolah1, klau5, lsaudit, m_Rassska, merlin, mrudenko, nobody2018, rokinot, rvierdiiev, sandy
163.1547 USDC - $163.15
https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/LiquidityPool.sol#L220-L226 https://github.com/code-423n4/2023-09-centrifuge/blob/main/src/LiquidityPool.sol#L237-L243
Anyone can call ERC20PermitLike(token).permit()
to verify signed message and update corresponding allowance mapping. The signed message can only be permitted one time, any attempt to permit it again will be reverted. Hence malicious user could frontrun ERC20PermitLike(token).permit()
with signed message to block calling of LiquidityPool#requestDepositWithPermit()
or LiquidityPool#requestRedeemWithPermit()
.
Add below codes before line 944 of LiquidityPool.t.sol and run test case, LiquidityPool#requestDepositWithPermit()
will be reverted:
erc20.permit(investor, address(investmentManager), amount, block.timestamp, v, r, s);
Add below codes before line 991 of LiquidityPool.t.sol and run test case, LiquidityPool#requestRedeemWithPermit()
will be reverted:
trancheToken.permit(investor, address(investmentManager), maxMint, block.timestamp, v, r, s);
Manual review
Check allowance before calling permit()
in LiquidityPool#requestDepositWithPermit()
and LiquidityPool#requestRedeemWithPermit()
as Uniswap did:
function requestDepositWithPermit(uint256 assets, address owner, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { if (IERC20(asset).allowance(owner, address(investmentManager)) < assets) { ERC20PermitLike(asset).permit(owner, address(investmentManager), assets, deadline, v, r, s); } investmentManager.requestDeposit(assets, owner); emit DepositRequested(owner, assets); } function requestRedeemWithPermit(uint256 shares, address owner, uint256 deadline, uint8 v, bytes32 r, bytes32 s)//@audit-ok public { if (share.allowance(owner, address(investmentManager)) < shares) { share.permit(owner, address(investmentManager), shares, deadline, v, r, s);//@audit-info check if signature is signed by owner and combined by all parameters. } investmentManager.requestRedeem(shares, owner);//@audit-info requestRedeem if signature is valid emit RedeemRequested(owner, shares); }
Other
#0 - c4-pre-sort
2023-09-15T20:37:47Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-15T20:37:59Z
raymondfam marked the issue as duplicate of #227
#2 - c4-judge
2023-09-25T15:39:24Z
gzeon-c4 marked the issue as partial-50
#3 - c4-sponsor
2023-09-25T15:44:50Z
hieronx (sponsor) confirmed
#4 - gzeon-c4
2023-09-25T15:44:58Z
similar permit frontrunning risk as #227 but misses the multiple liquidity pool grieving valid dos attack but seems to be with much lower impact
#5 - c4-judge
2023-09-25T15:45:18Z
gzeon-c4 marked the issue as full credit
#6 - c4-judge
2023-09-25T15:45:25Z
gzeon-c4 marked the issue as not a duplicate
#7 - c4-judge
2023-09-25T15:46:12Z
gzeon-c4 marked the issue as satisfactory
#8 - c4-judge
2023-09-25T15:56:38Z
gzeon-c4 changed the severity to QA (Quality Assurance)
#9 - c4-judge
2023-09-25T15:56:45Z
gzeon-c4 marked the issue as grade-a