Skip to content

Commit

Permalink
feat: refund for other address in Solidity contracts (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Dec 6, 2024
1 parent d0f7634 commit 5d144d2
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 65 deletions.
93 changes: 57 additions & 36 deletions contracts/ERC20Swap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract ERC20Swap {

// State variables

/// @dev Mapping between value hashes of swaps and whether they have Ether locked in the contract
/// @dev Mapping between value hashes of swaps and whether they have tokens locked in the contract
mapping(bytes32 => bool) public swaps;

// Events
Expand Down Expand Up @@ -88,35 +88,13 @@ contract ERC20Swap {
claim(preimage, amount, tokenAddress, msg.sender, refundAddress, timelock);
}

/// Claims Ether locked in the contract for a specified claim address
/// @dev To query the arguments of this function, get the "Lockup" event logs for the SHA256 hash of the preimage
/// @param preimage Preimage of the swap
/// @param amount Amount locked in the contract for the swap in the smallest denomination of the token
/// @param tokenAddress Address of the token locked for the swap
/// @param claimAddress Address to which the claimed funds will be sent
/// @param refundAddress Address that locked the tokens in the contract
/// @param timelock Block height after which the locked tokens can be refunded
function claim(
bytes32 preimage,
uint256 amount,
address tokenAddress,
address claimAddress,
address refundAddress,
uint256 timelock
) public {
prepareClaim(preimage, amount, tokenAddress, claimAddress, refundAddress, timelock);

// Transfer the tokens to the claim address
TransferHelper.safeTransferToken(tokenAddress, claimAddress, amount);
}

/// Claims multiple swaps
/// @dev All swaps that are claimed have to have "msg.sender" as "claimAddress" and the same token address
/// @param tokenAddress Address of the token of the swap
/// @param preimages Preimages of the swaps
/// @param amounts Amounts that are locked in the contract for the swap in WEI
/// @param refundAddresses Addresses that locked the Ether in the contract
/// @param timelocks Block heights after which the locked Ether can be refunded
/// @param refundAddresses Addresses that locked the tokens in the contract
/// @param timelocks Block heights after which the locked tokens can be refunded
function claimBatch(
address tokenAddress,
bytes32[] calldata preimages,
Expand Down Expand Up @@ -148,14 +126,11 @@ contract ERC20Swap {
/// @param amount Amount locked in the contract for the swap in the smallest denomination of the token
/// @param tokenAddress Address of the token locked for the swap
/// @param claimAddress Address that that was destined to claim the funds
/// @param timelock Block height after which the locked Ether can be refunded
/// @param timelock Block height after which the locked tokens can be refunded
function refund(bytes32 preimageHash, uint256 amount, address tokenAddress, address claimAddress, uint256 timelock)
external
{
// Make sure the timelock has expired already
// If the timelock is wrong, so will be the value hash of the swap which results in no swap being found
require(timelock <= block.number, "ERC20Swap: swap has not timed out yet");
refundInternal(preimageHash, amount, tokenAddress, claimAddress, timelock);
refund(preimageHash, amount, tokenAddress, claimAddress, msg.sender, timelock);
}

/// Refunds tokens locked in the contract with an EIP-712 signature of the claimAddress
Expand All @@ -165,7 +140,7 @@ contract ERC20Swap {
/// @param amount Amount locked in the contract for the swap in the smallest denomination of the token
/// @param tokenAddress Address of the token locked for the swap
/// @param claimAddress Address that that was destined to claim the funds
/// @param timelock Block height after which the locked Ether can be refunded
/// @param timelock Block height after which the locked tokens can be refunded
/// @param v final byte of the signature
/// @param r second 32 bytes of the signature
/// @param s first 32 bytes of the signature
Expand Down Expand Up @@ -193,7 +168,7 @@ contract ERC20Swap {
);
require(recoveredAddress != address(0) && recoveredAddress == claimAddress, "ERC20Swap: invalid signature");

refundInternal(preimageHash, amount, tokenAddress, claimAddress, timelock);
refundInternal(preimageHash, amount, tokenAddress, claimAddress, msg.sender, timelock);
}

// Public functions
Expand Down Expand Up @@ -228,6 +203,51 @@ contract ERC20Swap {
TransferHelper.safeTransferTokenFrom(tokenAddress, msg.sender, address(this), amount);
}

/// Claims tokens locked in the contract for a specified claim address
/// @dev To query the arguments of this function, get the "Lockup" event logs for the SHA256 hash of the preimage
/// @param preimage Preimage of the swap
/// @param amount Amount locked in the contract for the swap in the smallest denomination of the token
/// @param tokenAddress Address of the token locked for the swap
/// @param claimAddress Address to which the claimed funds will be sent
/// @param refundAddress Address that locked the tokens in the contract
/// @param timelock Block height after which the locked tokens can be refunded
function claim(
bytes32 preimage,
uint256 amount,
address tokenAddress,
address claimAddress,
address refundAddress,
uint256 timelock
) public {
prepareClaim(preimage, amount, tokenAddress, claimAddress, refundAddress, timelock);

// Transfer the tokens to the claim address
TransferHelper.safeTransferToken(tokenAddress, claimAddress, amount);
}

/// Refunds tokens locked in the contract after the timeout for a specified refund address
/// @dev To query the arguments of this function, get the "Lockup" event logs for your refund address and the preimage hash if you have it
/// @dev For further explanations and reasoning behind the statements in this function, check the "claim" function
/// @param preimageHash Preimage hash of the swap
/// @param amount Amount locked in the contract for the swap in the smallest denomination of the token
/// @param tokenAddress Address of the token locked for the swap
/// @param claimAddress Address that was destined to claim the funds
/// @param refundAddress Address that locked the tokens in the contract
/// @param timelock Block height after which the locked tokens can be refunded
function refund(
bytes32 preimageHash,
uint256 amount,
address tokenAddress,
address claimAddress,
address refundAddress,
uint256 timelock
) public {
// Make sure the timelock has expired already
// If the timelock is wrong, so will be the value hash of the swap which results in no swap being found
require(timelock <= block.number, "ERC20Swap: swap has not timed out yet");
refundInternal(preimageHash, amount, tokenAddress, claimAddress, refundAddress, timelock);
}

/// Hashes all the values of a swap with Keccak256
/// @param preimageHash Preimage hash of the swap
/// @param amount Amount the swap has locked in the smallest denomination of the token
Expand Down Expand Up @@ -255,8 +275,8 @@ contract ERC20Swap {
/// @param amount Amount locked in the contract for the swap in the smallest denomination of the token
/// @param tokenAddress Address of the token of the swap
/// @param claimAddress Address that that was destined to claim the funds
/// @param refundAddress Address that locked the Ether and can refund them
/// @param timelock Block height after which the locked Ether can be refunded
/// @param refundAddress Address that locked the tokens and can refund them
/// @param timelock Block height after which the locked tokens can be refunded
function prepareClaim(
bytes32 preimage,
uint256 amount,
Expand Down Expand Up @@ -287,16 +307,17 @@ contract ERC20Swap {
uint256 amount,
address tokenAddress,
address claimAddress,
address refundAddress,
uint256 timelock
) private {
bytes32 hash = hashValues(preimageHash, amount, tokenAddress, claimAddress, msg.sender, timelock);
bytes32 hash = hashValues(preimageHash, amount, tokenAddress, claimAddress, refundAddress, timelock);

checkSwapIsLocked(hash);
delete swaps[hash];

emit Refund(preimageHash);

TransferHelper.safeTransferToken(tokenAddress, msg.sender, amount);
TransferHelper.safeTransferToken(tokenAddress, refundAddress, amount);
}

/// Checks whether a swap has tokens locked in the contract
Expand Down
68 changes: 44 additions & 24 deletions contracts/EtherSwap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,22 +93,6 @@ contract EtherSwap {
claim(preimage, amount, msg.sender, refundAddress, timelock);
}

/// Claims Ether locked in the contract for a specified claim address
/// @dev To query the arguments of this function, get the "Lockup" event logs for the SHA256 hash of the preimage
/// @param preimage Preimage of the swap
/// @param amount Amount locked in the contract for the swap in WEI
/// @param claimAddress Address to which the claimed funds will be sent
/// @param refundAddress Address that locked the Ether in the contract
/// @param timelock Block height after which the locked Ether can be refunded
function claim(bytes32 preimage, uint256 amount, address claimAddress, address refundAddress, uint256 timelock)
public
{
prepareClaim(preimage, amount, claimAddress, refundAddress, timelock);

// Transfer the Ether to the claim address
TransferHelper.transferEther(payable(claimAddress), amount);
}

/// Claims multiple swaps
/// @dev All swaps that are claimed have to have "msg.sender" as "claimAddress"
/// @param preimages Preimages of the swaps
Expand Down Expand Up @@ -146,10 +130,7 @@ contract EtherSwap {
/// @param claimAddress Address that that was destined to claim the funds
/// @param timelock Block height after which the locked Ether can be refunded
function refund(bytes32 preimageHash, uint256 amount, address claimAddress, uint256 timelock) external {
// Make sure the timelock has expired already
// If the timelock is wrong, so will be the value hash of the swap which results in no swap being found
require(timelock <= block.number, "EtherSwap: swap has not timed out yet");
refundInternal(preimageHash, amount, claimAddress, timelock);
refund(preimageHash, amount, claimAddress, msg.sender, timelock);
}

/// Refunds Ether locked in the contract with an EIP-712 signature of the claimAddress
Expand Down Expand Up @@ -185,11 +166,44 @@ contract EtherSwap {
);
require(recoveredAddress != address(0) && recoveredAddress == claimAddress, "EtherSwap: invalid signature");

refundInternal(preimageHash, amount, claimAddress, timelock);
refundInternal(preimageHash, amount, claimAddress, msg.sender, timelock);
}

// Public functions

/// Claims Ether locked in the contract for a specified claim address
/// @dev To query the arguments of this function, get the "Lockup" event logs for the SHA256 hash of the preimage
/// @param preimage Preimage of the swap
/// @param amount Amount locked in the contract for the swap in WEI
/// @param claimAddress Address to which the claimed funds will be sent
/// @param refundAddress Address that locked the Ether in the contract
/// @param timelock Block height after which the locked Ether can be refunded
function claim(bytes32 preimage, uint256 amount, address claimAddress, address refundAddress, uint256 timelock)
public
{
prepareClaim(preimage, amount, claimAddress, refundAddress, timelock);

// Transfer the Ether to the claim address
TransferHelper.transferEther(payable(claimAddress), amount);
}

/// Refunds Ether locked in the contract after the timeout for a specified refund address address
/// @dev To query the arguments of this function, get the "Lockup" event logs for your refund address and the preimage hash if you have it
/// @dev For further explanations and reasoning behind the statements in this function, check the "claim" function
/// @param preimageHash Preimage hash of the swap
/// @param amount Amount locked in the contract for the swap in WEI
/// @param claimAddress Address that that was destined to claim the funds
/// @param refundAddress Address that locked the Ether in the contract
/// @param timelock Block height after which the locked Ether can be refunded
function refund(bytes32 preimageHash, uint256 amount, address claimAddress, address refundAddress, uint256 timelock)
public
{
// Make sure the timelock has expired already
// If the timelock is wrong, so will be the value hash of the swap which results in no swap being found
require(timelock <= block.number, "EtherSwap: swap has not timed out yet");
refundInternal(preimageHash, amount, claimAddress, refundAddress, timelock);
}

/// Hashes all the values of a swap with Keccak256
/// @param preimageHash Preimage hash of the swap
/// @param amount Amount the swap has locked in WEI
Expand Down Expand Up @@ -261,15 +275,21 @@ contract EtherSwap {
emit Lockup(preimageHash, amount, claimAddress, msg.sender, timelock);
}

function refundInternal(bytes32 preimageHash, uint256 amount, address claimAddress, uint256 timelock) private {
bytes32 hash = hashValues(preimageHash, amount, claimAddress, msg.sender, timelock);
function refundInternal(
bytes32 preimageHash,
uint256 amount,
address claimAddress,
address refundAddress,
uint256 timelock
) private {
bytes32 hash = hashValues(preimageHash, amount, claimAddress, refundAddress, timelock);

checkSwapIsLocked(hash);
delete swaps[hash];

emit Refund(preimageHash);

TransferHelper.transferEther(payable(msg.sender), amount);
TransferHelper.transferEther(payable(refundAddress), amount);
}

/// Checks whether a swap has Ether locked in the contract
Expand Down
20 changes: 20 additions & 0 deletions contracts/test/ERC20SwapTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,26 @@ contract ERC20SwapTest is Test {
assertEq(token.balanceOf(address(this)) - balanceBeforeRefund, lockupAmount);
}

function testRefundAddress() external {
uint256 timelock = block.number;

token.approve(address(swap), lockupAmount);
lock(timelock);

uint256 balanceBeforeRefund = token.balanceOf(address(this));

vm.expectEmit(true, false, false, false, address(swap));
emit Refund(preimageHash);

vm.prank(claimAddress);
swap.refund(preimageHash, lockupAmount, address(token), claimAddress, address(this), timelock);

assertFalse(querySwap(timelock));

assertEq(token.balanceOf(address(swap)), 0);
assertEq(token.balanceOf(address(this)) - balanceBeforeRefund, lockupAmount);
}

function testRefundTwiceFail() external {
uint256 timelock = block.number;

Expand Down
17 changes: 17 additions & 0 deletions contracts/test/EtherSwapTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,23 @@ contract EtherSwapTest is Test {
assertEq(address(this).balance - balanceBeforeRefund, lockupAmount);
}

function testRefundAddress() external {
uint256 timelock = block.number;

lock(timelock);

uint256 balanceBeforeRefund = address(this).balance;

vm.expectEmit(true, false, false, false, address(swap));
emit Refund(preimageHash);

vm.prank(claimAddress);
swap.refund(preimageHash, lockupAmount, claimAddress, address(this), timelock);

assertEq(address(swap).balance, 0);
assertEq(address(this).balance - balanceBeforeRefund, lockupAmount);
}

function testRefundTwiceFail() external {
uint256 timelock = block.number;

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"git-cliff": "^2.7.0",
"globals": "^15.13.0",
"jest": "^29.7.0",
"prettier": "^3.4.1",
"prettier": "^3.4.2",
"slip77": "^0.2.0",
"tiny-secp256k1": "^2.2.3",
"ts-jest": "^29.2.5",
Expand Down

0 comments on commit 5d144d2

Please sign in to comment.