-
Notifications
You must be signed in to change notification settings - Fork 29
feat: synapse migrator #339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9bb3cbb
ad46eed
14c7254
b68d360
83b362e
8a7aa05
803dd05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// @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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Decimal conversion logic needs overflow protection. The decimal conversion calculation 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
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; | ||
} |
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); | ||
} |
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(); | ||
} |
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(); | ||
} | ||
} |
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); | ||
} | ||
} |
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) {} | ||
} |
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) {} | ||
} |
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); | ||
} | ||
} |
There was a problem hiding this comment.
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.
📝 Committable suggestion
🤖 Prompt for AI Agents