-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Beginnings of a DecentSablier module
- Loading branch information
Showing
4 changed files
with
247 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity =0.8.19; | ||
|
||
import {Enum} from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; | ||
import {IAvatar} from "@gnosis.pm/zodiac/contracts/interfaces/IAvatar.sol"; | ||
import {ISablier} from "./interfaces/sablier/ISablier.sol"; | ||
|
||
contract DecentSablier_0_1_0 { | ||
string public constant NAME = "DecentSablier_0_1_0"; | ||
|
||
struct SablierStreamInfo { | ||
uint256 streamId; | ||
} | ||
|
||
function processSablierStreams( | ||
address sablierContract, | ||
SablierStreamInfo[] calldata streams | ||
) public { | ||
ISablier sablier = ISablier(sablierContract); | ||
|
||
for (uint256 i = 0; i < streams.length; i++) { | ||
uint256 streamId = streams[i].streamId; | ||
|
||
// Get the current balance available for withdrawal | ||
uint256 availableBalance = sablier.balanceOf(streamId, msg.sender); | ||
|
||
if (availableBalance > 0) { | ||
// Proxy the withdrawal call through the Safe | ||
IAvatar(msg.sender).execTransactionFromModule( | ||
sablierContract, | ||
0, | ||
abi.encodeWithSelector( | ||
ISablier.withdrawFromStream.selector, | ||
streamId, | ||
availableBalance | ||
), | ||
Enum.Operation.Call | ||
); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity =0.8.19; | ||
|
||
interface ISablier { | ||
function getStream( | ||
uint256 streamId | ||
) | ||
external | ||
view | ||
returns ( | ||
address sender, | ||
address recipient, | ||
uint256 deposit, | ||
address tokenAddress, | ||
uint256 startTime, | ||
uint256 stopTime, | ||
uint256 remainingBalance, | ||
uint256 ratePerSecond | ||
); | ||
function balanceOf( | ||
uint256 streamId, | ||
address who | ||
) external view returns (uint256 balance); | ||
function withdrawFromStream( | ||
uint256 streamId, | ||
uint256 amount | ||
) external returns (bool); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity =0.8.19; | ||
|
||
contract MockSablier { | ||
mapping(uint256 => uint256) private streamBalances; | ||
mapping(uint256 => uint256) private withdrawnAmounts; | ||
|
||
function setStreamBalance(uint256 streamId, uint256 balance) external { | ||
streamBalances[streamId] = balance; | ||
} | ||
|
||
function balanceOf( | ||
uint256 streamId, | ||
address | ||
) external view returns (uint256) { | ||
return streamBalances[streamId]; | ||
} | ||
|
||
function withdrawFromStream( | ||
uint256 streamId, | ||
uint256 amount | ||
) external returns (bool) { | ||
require(streamBalances[streamId] >= amount, "Insufficient balance"); | ||
streamBalances[streamId] -= amount; | ||
withdrawnAmounts[streamId] += amount; | ||
return true; | ||
} | ||
|
||
function getWithdrawnAmount( | ||
uint256 streamId | ||
) external view returns (uint256) { | ||
return withdrawnAmounts[streamId]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { | ||
GnosisSafeL2, | ||
GnosisSafeL2__factory, | ||
DecentSablier_0_1_0__factory, | ||
DecentSablier_0_1_0, | ||
} from "../typechain-types"; | ||
|
||
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; | ||
import { expect } from "chai"; | ||
import { ethers } from "ethers"; | ||
import hre from "hardhat"; | ||
|
||
import { | ||
getGnosisSafeL2Singleton, | ||
getGnosisSafeProxyFactory, | ||
} from "./GlobalSafeDeployments.test"; | ||
import { | ||
buildSafeTransaction, | ||
buildSignatureBytes, | ||
predictGnosisSafeAddress, | ||
safeSignTypedData, | ||
} from "./helpers"; | ||
|
||
import { MockSablier__factory } from "../typechain-types"; | ||
|
||
async function executeSafeTransaction({ | ||
safe, | ||
to, | ||
value, | ||
data, | ||
operation, | ||
signers, | ||
}: { | ||
safe: GnosisSafeL2; | ||
to: string; | ||
value?: bigint; | ||
data?: string; | ||
operation?: number; | ||
signers: SignerWithAddress[]; | ||
}) { | ||
const safeTransactionData = { | ||
to, | ||
value: value || 0n, | ||
data: data || "0x", | ||
operation: operation || 0, | ||
// Add the missing 'nonce' property | ||
nonce: await safe.nonce(), | ||
}; | ||
const safeTransaction = await buildSafeTransaction(safeTransactionData); | ||
const senderSignature = await safeSignTypedData( | ||
signers[0], | ||
safe, | ||
safeTransaction | ||
); | ||
const signatureBytes = buildSignatureBytes([senderSignature]); | ||
// Change 'executeTransaction' to 'execTransaction' | ||
return safe.execTransaction(safeTransaction, signatureBytes); | ||
} | ||
|
||
describe("DecentSablier", () => { | ||
let dao: SignerWithAddress; | ||
let gnosisSafe: GnosisSafeL2; | ||
let decentSablier: DecentSablier_0_1_0; | ||
let decentSablierAddress: string; | ||
let gnosisSafeAddress: string; | ||
|
||
let mockSablier: MockSablier; | ||
|
||
beforeEach(async () => { | ||
// ... (setup code similar to DecentHats.test.ts) | ||
// Deploy MockSablier | ||
const MockSablier = await ethers.getContractFactory("MockSablier"); | ||
mockSablier = await MockSablier.deploy(); | ||
await mockSablier.deployed(); | ||
}); | ||
|
||
describe("DecentSablier as a Module", () => { | ||
let enableModuleTx: ethers.ContractTransactionResponse; | ||
|
||
beforeEach(async () => { | ||
// ... (enable module code similar to DecentHats.test.ts) | ||
}); | ||
|
||
it("Emits an ExecutionSuccess event", async () => { | ||
await expect(enableModuleTx).to.emit(gnosisSafe, "ExecutionSuccess"); | ||
}); | ||
|
||
it("Emits an EnabledModule event", async () => { | ||
await expect(enableModuleTx) | ||
.to.emit(gnosisSafe, "EnabledModule") | ||
.withArgs(decentSablierAddress); | ||
}); | ||
|
||
describe("Processing Sablier Streams", () => { | ||
let processSablierStreamsTx: ethers.ContractTransactionResponse; | ||
|
||
beforeEach(async () => { | ||
// Set up mock stream balances | ||
await mockSablier.setStreamBalance(1, ethers.utils.parseEther("100")); | ||
await mockSablier.setStreamBalance(2, ethers.utils.parseEther("200")); | ||
await mockSablier.setStreamBalance(3, ethers.utils.parseEther("300")); | ||
|
||
processSablierStreamsTx = await executeSafeTransaction({ | ||
safe: gnosisSafe, | ||
to: decentSablierAddress, | ||
data: DecentSablier_0_1_0__factory.createInterface().encodeFunctionData( | ||
"processSablierStreams", | ||
[ | ||
mockSablier.address, | ||
[{ streamId: 1 }, { streamId: 2 }, { streamId: 3 }], | ||
] | ||
), | ||
signers: [dao], | ||
}); | ||
}); | ||
|
||
it("Emits an ExecutionSuccess event", async () => { | ||
await expect(processSablierStreamsTx).to.emit( | ||
gnosisSafe, | ||
"ExecutionSuccess" | ||
); | ||
}); | ||
|
||
it("Emits an ExecutionFromModuleSuccess event", async () => { | ||
await expect(processSablierStreamsTx) | ||
.to.emit(gnosisSafe, "ExecutionFromModuleSuccess") | ||
.withArgs(decentSablierAddress); | ||
}); | ||
|
||
it("Withdraws from streams correctly", async () => { | ||
expect(await mockSablier.getWithdrawnAmount(1)).to.equal( | ||
ethers.utils.parseEther("100") | ||
); | ||
expect(await mockSablier.getWithdrawnAmount(2)).to.equal( | ||
ethers.utils.parseEther("200") | ||
); | ||
expect(await mockSablier.getWithdrawnAmount(3)).to.equal( | ||
ethers.utils.parseEther("300") | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |