Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions contracts/migrator/SynapseMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {IBurnable} from "./interfaces/IBurnable.sol";
import {ISynapseMigrator} from "./interfaces/ISynapseMigrator.sol";
import {ISynapseMigratorErrors} from "./interfaces/ISynapseMigratorErrors.sol";

import {Ownable} from "@openzeppelin/contracts-4.5.0/access/Ownable.sol";
import {IERC20Metadata} from "@openzeppelin/contracts-4.5.0/token/ERC20/extensions/IERC20Metadata.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts-4.5.0/token/ERC20/utils/SafeERC20.sol";

contract SynapseMigrator is Ownable, ISynapseMigrator, ISynapseMigratorErrors {
using SafeERC20 for IERC20;

struct TokenPair {
address newToken;
uint8 oldTokenDecimals;
uint8 newTokenDecimals;
}

mapping(address => TokenPair) internal _tokenPairs;

event Migrated(address indexed user, address indexed oldToken, uint256 amount);
event TokenPairAdded(address indexed oldToken, address indexed newToken);

constructor(address owner_) {
transferOwnership(owner_);
}

/// @inheritdoc ISynapseMigrator
function addTokenPair(address oldToken, address newToken) external onlyOwner {
if (oldToken == address(0) || newToken == address(0)) revert SM__ZeroAddress();
if (oldToken == newToken) revert SM__SameAddress();
// Check that the token pair has not been added yet
if (_tokenPairs[oldToken].newToken != address(0)) revert SM__TokenPairAlreadyAdded();
// Add token pair and record the tokens decimals to avoid extra calls in the future
_tokenPairs[oldToken] = TokenPair({
newToken: newToken,
oldTokenDecimals: IERC20Metadata(oldToken).decimals(),
newTokenDecimals: IERC20Metadata(newToken).decimals()
});
emit TokenPairAdded(oldToken, newToken);
}

/// @inheritdoc ISynapseMigrator
function migrate(address oldToken, uint256 amount) external {
if (oldToken == address(0)) revert SM__ZeroAddress();
(address newToken, uint256 newAmount) = previewMigrate(oldToken, amount);
if (newToken == address(0)) revert SM__TokenPairNotAdded();
if (newAmount == 0) revert SM__ZeroAmount();
// Burn old tokens from the user
IBurnable(oldToken).burnFrom(msg.sender, amount);
// Send new tokens to the user
IERC20(newToken).safeTransfer(msg.sender, newAmount);
emit Migrated(msg.sender, oldToken, amount);
}
Comment on lines +46 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Migration function has potential reentrancy risk.

The function calls external contracts (burn and transfer) which could potentially lead to reentrancy attacks. Consider adding a reentrancy guard.

+import {ReentrancyGuard} from "@openzeppelin/contracts-4.5.0/security/ReentrancyGuard.sol";

-contract SynapseMigrator is Ownable, ISynapseMigrator, ISynapseMigratorErrors {
+contract SynapseMigrator is Ownable, ReentrancyGuard, ISynapseMigrator, ISynapseMigratorErrors {

-    function migrate(address oldToken, uint256 amount) external {
+    function migrate(address oldToken, uint256 amount) external nonReentrant {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function migrate(address oldToken, uint256 amount) external {
if (oldToken == address(0)) revert SM__ZeroAddress();
(address newToken, uint256 newAmount) = previewMigrate(oldToken, amount);
if (newToken == address(0)) revert SM__TokenPairNotAdded();
if (newAmount == 0) revert SM__ZeroAmount();
// Burn old tokens from the user
IBurnable(oldToken).burnFrom(msg.sender, amount);
// Send new tokens to the user
IERC20(newToken).safeTransfer(msg.sender, newAmount);
emit Migrated(msg.sender, oldToken, amount);
}
function migrate(address oldToken, uint256 amount) external nonReentrant {
if (oldToken == address(0)) revert SM__ZeroAddress();
(address newToken, uint256 newAmount) = previewMigrate(oldToken, amount);
if (newToken == address(0)) revert SM__TokenPairNotAdded();
if (newAmount == 0) revert SM__ZeroAmount();
// Burn old tokens from the user
IBurnable(oldToken).burnFrom(msg.sender, amount);
// Send new tokens to the user
IERC20(newToken).safeTransfer(msg.sender, newAmount);
emit Migrated(msg.sender, oldToken, amount);
}
🤖 Prompt for AI Agents
In contracts/migrator/SynapseMigrator.sol around lines 46 to 56, the migrate
function calls external contracts for burning and transferring tokens without
protection against reentrancy attacks. To fix this, add a reentrancy guard
modifier to the migrate function and apply the checks-effects-interactions
pattern by performing all state changes before external calls. This will prevent
potential reentrancy exploits during token operations.


/// @inheritdoc ISynapseMigrator
function getTokenPair(address oldToken) external view returns (address newToken) {
return _tokenPairs[oldToken].newToken;
}

/// @inheritdoc ISynapseMigrator
function previewMigrate(address oldToken, uint256 amount)
public
view
returns (address newToken, uint256 newAmount)
{
newToken = _tokenPairs[oldToken].newToken;
uint256 oldDecimals = _tokenPairs[oldToken].oldTokenDecimals;
uint256 newDecimals = _tokenPairs[oldToken].newTokenDecimals;
if (newToken == address(0)) return (address(0), 0);
newAmount = (amount * 10**newDecimals) / 10**oldDecimals;
}
Comment on lines +64 to +74
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Decimal conversion logic needs overflow protection.

The decimal conversion calculation (amount * 10**newDecimals) / 10**oldDecimals could potentially overflow for large amounts, especially with high decimal differences.

Consider using a safer calculation approach or adding overflow checks:

 function previewMigrate(address oldToken, uint256 amount)
     public
     view
     returns (address newToken, uint256 newAmount)
 {
     newToken = _tokenPairs[oldToken].newToken;
     uint256 oldDecimals = _tokenPairs[oldToken].oldTokenDecimals;
     uint256 newDecimals = _tokenPairs[oldToken].newTokenDecimals;
     if (newToken == address(0)) return (address(0), 0);
-    newAmount = (amount * 10**newDecimals) / 10**oldDecimals;
+    if (newDecimals >= oldDecimals) {
+        newAmount = amount * (10**(newDecimals - oldDecimals));
+    } else {
+        newAmount = amount / (10**(oldDecimals - newDecimals));
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function previewMigrate(address oldToken, uint256 amount)
public
view
returns (address newToken, uint256 newAmount)
{
newToken = _tokenPairs[oldToken].newToken;
uint256 oldDecimals = _tokenPairs[oldToken].oldTokenDecimals;
uint256 newDecimals = _tokenPairs[oldToken].newTokenDecimals;
if (newToken == address(0)) return (address(0), 0);
newAmount = (amount * 10**newDecimals) / 10**oldDecimals;
}
function previewMigrate(address oldToken, uint256 amount)
public
view
returns (address newToken, uint256 newAmount)
{
newToken = _tokenPairs[oldToken].newToken;
uint256 oldDecimals = _tokenPairs[oldToken].oldTokenDecimals;
uint256 newDecimals = _tokenPairs[oldToken].newTokenDecimals;
if (newToken == address(0)) return (address(0), 0);
if (newDecimals >= oldDecimals) {
newAmount = amount * (10**(newDecimals - oldDecimals));
} else {
newAmount = amount / (10**(oldDecimals - newDecimals));
}
}
🤖 Prompt for AI Agents
In contracts/migrator/SynapseMigrator.sol around lines 64 to 74, the decimal
conversion calculation multiplies amount by 10 to the power of newDecimals,
which can overflow for large values. To fix this, use a safe math approach such
as performing the multiplication and division in a way that avoids intermediate
overflow, for example by dividing first if possible or using a library like
SafeMath or unchecked blocks with explicit overflow checks. Ensure the
calculation safely handles large amounts and decimal differences without risking
overflow.

}
6 changes: 6 additions & 0 deletions contracts/migrator/interfaces/IBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface IBurnable {
function burnFrom(address from, uint256 amount) external;
}
32 changes: 32 additions & 0 deletions contracts/migrator/interfaces/ISynapseMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface ISynapseMigrator {
/// @notice Allows the contract owner to add a token pair to the migrator.
/// Users will be able to migrate from the old token to the new token using 1:1 ratio,
/// taking token decimals into account.
/// @dev Will revert in the following cases:
/// - Either of the tokens is the zero address.
/// - Token addresses are the same.
/// - The token pair is already added for the old token.
function addTokenPair(address oldToken, address newToken) external;

/// @notice Migrates the given amount of old tokens to new tokens.
/// Old tokens will be taken from the user and burned. New tokens will be transferred to the user.
/// @dev Will revert in the following cases:
/// - Zero address or amount is supplied.
/// - The token pair is not added for the old token.
/// - Contract does not have enough balance of the new token.
function migrate(address oldToken, uint256 amount) external;

/// @notice Returns the new token for the given old token.
/// @dev Will return the zero address if the token pair is not added for the old token.
function getTokenPair(address oldToken) external view returns (address newToken);

/// @notice Returns the new token and amount of new tokens that will be received for the given amount of old tokens.
/// @dev Will return (address(0), 0) if the token pair is not added for the old token.
function previewMigrate(address oldToken, uint256 amount)
external
view
returns (address newToken, uint256 newAmount);
}
10 changes: 10 additions & 0 deletions contracts/migrator/interfaces/ISynapseMigratorErrors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface ISynapseMigratorErrors {
error SM__SameAddress();
error SM__TokenPairAlreadyAdded();
error SM__TokenPairNotAdded();
error SM__ZeroAddress();
error SM__ZeroAmount();
}
33 changes: 33 additions & 0 deletions test/migrator/SynapseMigrator.LessDecimals.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {SynapseMigratorTest} from "./SynapseMigrator.t.sol";

// solhint-disable func-name-mixedcase
contract SynapseMigratorLessDecimalsTest is SynapseMigratorTest {
constructor() SynapseMigratorTest(18, 6) {}

function test_migrate_precisionLoss() public {
amount = 1.23456789 * 10**18;
expectedNewAmount = 1234567;
test_migrate();
}

function test_migrate_revert_precisionLoss_zeroAmountOut() public {
vm.expectRevert(SM__ZeroAmount.selector);
vm.prank(user);
migrator.migrate(address(oldToken), 10**12 - 1);
}

function test_previewMigrate_precisionLoss() public {
amount = 1.23456789 * 10**18;
expectedNewAmount = 1234567;
test_previewMigrate();
}

function test_previewMigrate_precisionLoss_zeroAmountOut() public {
amount = 10**12 - 1;
expectedNewAmount = 0;
test_previewMigrate();
}
}
97 changes: 97 additions & 0 deletions test/migrator/SynapseMigrator.Management.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {SynapseMigrator, ISynapseMigratorErrors} from "../../contracts/migrator/SynapseMigrator.sol";

import {IERC20Metadata} from "@openzeppelin/contracts-4.5.0/token/ERC20/extensions/IERC20Metadata.sol";

import {Test} from "forge-std/Test.sol";

// solhint-disable func-name-mixedcase
contract SynapseMigratorManagementTest is Test, ISynapseMigratorErrors {
event TokenPairAdded(address indexed oldToken, address indexed newToken);

SynapseMigrator internal migrator;
address internal owner = makeAddr("owner");

address internal oldToken = makeAddr("oldToken");
address internal newToken = makeAddr("newToken");
address internal anotherToken = makeAddr("anotherToken");

function setUp() public {
migrator = new SynapseMigrator(owner);

vm.mockCall({
callee: oldToken,
data: abi.encodeWithSelector(IERC20Metadata.decimals.selector),
returnData: abi.encode(18)
});
vm.mockCall({
callee: newToken,
data: abi.encodeWithSelector(IERC20Metadata.decimals.selector),
returnData: abi.encode(6)
});
vm.mockCall({
callee: anotherToken,
data: abi.encodeWithSelector(IERC20Metadata.decimals.selector),
returnData: abi.encode(6)
});
}

function test_constructor() public {
assertEq(migrator.owner(), owner);
assertEq(migrator.getTokenPair(oldToken), address(0));
}

function test_constructor_revert_zeroOwner() public {
vm.expectRevert("Ownable: new owner is the zero address");
new SynapseMigrator(address(0));
}

function test_addTokenPair() public {
vm.expectEmit(address(migrator));
emit TokenPairAdded(oldToken, newToken);
vm.prank(owner);
migrator.addTokenPair(oldToken, newToken);
assertEq(migrator.getTokenPair(oldToken), newToken);
assertEq(migrator.getTokenPair(newToken), address(0));
}

function test_addTokenPair_revert_oldTokenZero() public {
vm.expectRevert(SM__ZeroAddress.selector);
vm.prank(owner);
migrator.addTokenPair(address(0), newToken);
}

function test_addTokenPair_revert_newTokenZero() public {
vm.expectRevert(SM__ZeroAddress.selector);
vm.prank(owner);
migrator.addTokenPair(oldToken, address(0));
}

function test_addTokenPair_revert_sameTokens() public {
vm.expectRevert(SM__SameAddress.selector);
vm.prank(owner);
migrator.addTokenPair(oldToken, oldToken);
}

function test_addTokenPair_revert_oldTokenAlreadyAdded() public {
vm.prank(owner);
migrator.addTokenPair(oldToken, newToken);
// Same address
vm.expectRevert(SM__TokenPairAlreadyAdded.selector);
vm.prank(owner);
migrator.addTokenPair(oldToken, newToken);
// New address
vm.expectRevert(SM__TokenPairAlreadyAdded.selector);
vm.prank(owner);
migrator.addTokenPair(oldToken, anotherToken);
}

function test_addTokenPair_revert_callerNotOwner(address caller) public {
vm.assume(caller != owner);
vm.expectRevert("Ownable: caller is not the owner");
vm.prank(caller);
migrator.addTokenPair(oldToken, newToken);
}
}
8 changes: 8 additions & 0 deletions test/migrator/SynapseMigrator.MoreDecimals.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {SynapseMigratorTest} from "./SynapseMigrator.t.sol";

contract SynapseMigratorMoreDecimalsTest is SynapseMigratorTest {
constructor() SynapseMigratorTest(6, 18) {}
}
8 changes: 8 additions & 0 deletions test/migrator/SynapseMigrator.SameDecimals.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {SynapseMigratorTest} from "./SynapseMigrator.t.sol";

contract SynapseMigratorSameDecimalsTest is SynapseMigratorTest {
constructor() SynapseMigratorTest(18, 18) {}
}
107 changes: 107 additions & 0 deletions test/migrator/SynapseMigrator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {SynapseMigrator, ISynapseMigratorErrors} from "../../contracts/migrator/SynapseMigrator.sol";

import {MockBurnableToken} from "../mocks/MockBurnableToken.sol";

import {Test} from "forge-std/Test.sol";

// solhint-disable func-name-mixedcase
abstract contract SynapseMigratorTest is Test, ISynapseMigratorErrors {
event Migrated(address indexed user, address indexed oldToken, uint256 amount);

SynapseMigrator internal migrator;
MockBurnableToken internal oldToken;
MockBurnableToken internal newToken;

uint8 internal oldTokenDecimals;
uint8 internal newTokenDecimals;

address internal user = makeAddr("user");
uint256 internal oldTokenBalance;
uint256 internal newTokenSupply;
uint256 internal amount;
uint256 internal expectedNewAmount;

constructor(uint8 oldTokenDecimals_, uint8 newTokenDecimals_) {
oldTokenDecimals = oldTokenDecimals_;
newTokenDecimals = newTokenDecimals_;
}

function setUp() public {
migrator = new SynapseMigrator(address(this));
oldToken = new MockBurnableToken("OldToken", oldTokenDecimals);
newToken = new MockBurnableToken("NewToken", newTokenDecimals);
migrator.addTokenPair(address(oldToken), address(newToken));

// 10 tokens
oldTokenBalance = 10 * 10**oldTokenDecimals;
newTokenSupply = 10 * 10**newTokenDecimals;
// Migrate 1 token
amount = 10**oldTokenDecimals;
expectedNewAmount = 10**newTokenDecimals;

oldToken.mintTestTokens(user, oldTokenBalance);
newToken.mintTestTokens(address(migrator), newTokenSupply);

vm.prank(user);
oldToken.approve(address(migrator), type(uint256).max);
}

function test_migrate() public {
vm.expectEmit(address(migrator));
emit Migrated(user, address(oldToken), amount);
vm.prank(user);
migrator.migrate(address(oldToken), amount);
// Old token balances
assertEq(oldToken.balanceOf(user), oldTokenBalance - amount);
assertEq(oldToken.balanceOf(address(migrator)), 0);
assertEq(oldToken.totalSupply(), oldTokenBalance - amount);
// New token balances
assertEq(newToken.balanceOf(user), expectedNewAmount);
assertEq(newToken.balanceOf(address(migrator)), newTokenSupply - expectedNewAmount);
assertEq(newToken.totalSupply(), newTokenSupply);
}

function test_migrate_revert_oldTokenNotAdded() public {
// Redeploy migrator to effectively remove the token pair
migrator = new SynapseMigrator(address(this));
vm.expectRevert(SM__TokenPairNotAdded.selector);
vm.prank(user);
migrator.migrate(address(oldToken), amount);
}

function test_migrate_revert_oldTokenZero() public {
vm.expectRevert(SM__ZeroAddress.selector);
vm.prank(user);
migrator.migrate(address(0), amount);
}

function test_migrate_revert_amountZero() public {
vm.expectRevert(SM__ZeroAmount.selector);
vm.prank(user);
migrator.migrate(address(oldToken), 0);
}

function test_migrate_revert_notEnoughAllowance() public {
vm.prank(user);
oldToken.approve(address(migrator), amount - 1);
vm.expectRevert();
vm.prank(user);
migrator.migrate(address(oldToken), amount);
}

function test_previewMigrate() public {
(address previewedNewToken, uint256 previewedAmount) = migrator.previewMigrate(address(oldToken), amount);
assertEq(previewedNewToken, address(newToken));
assertEq(previewedAmount, expectedNewAmount);
}

function test_previewMigrate_returnsZero_tokenNotAdded() public {
migrator = new SynapseMigrator(address(this));
(address previewedNewToken, uint256 previewedAmount) = migrator.previewMigrate(address(oldToken), amount);
assertEq(previewedNewToken, address(0));
assertEq(previewedAmount, 0);
}
}
Loading
Loading